percy / percy-agent

[Deprecated in favor of `@percy/cli`] An agent process for integrating with Percy.
https://percy.io
MIT License
22 stars 23 forks source link

intermittent missing styles #551

Closed xiwcx closed 3 years ago

xiwcx commented 4 years ago

Hi, I'm having an issue where I can not get the snapshots to reliably include my styles. I'm using percy-selenium-python, CircleCi, Docker, and Django.

Running repeated builds on the exact same commit gets me styled snapshots about a quarter of the time. When i SSH in to the build I can see that the assets are correctly and consistently built even when the snapshots are missing styles. Is there any configuration I can add to percy agent to make sure the styles have loaded or are included?

I can provide as much extra context as needed. Thank you!

Python test file:

import socket
import os

from django.test import override_settings, tag, LiveServerTestCase

from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium import webdriver
from percy import percySnapshot

class MySeleniumTests(LiveServerTestCase):
    host = "0.0.0.0"
    port = 8000

    @property
    def base_url(self):
        domain = "localhost" if "CIRCLECI" in os.environ else "host.docker.internal"
        return f"http://{domain}:{self.port}"

    @classmethod
    def setUpClass(cls):
        super().setUpClass()

        options = webdriver.ChromeOptions()
        options.add_argument("--no-sandbox")
        options.add_argument("--disable-dev-shm-usage")
        options.add_argument("--disable-web-security")
        options.add_argument("--disable-extensions")

        cls.driver = webdriver.Remote(
            command_executor="http://localhost:4444/wd/hub",
            desired_capabilities=options.to_capabilities(),
        )
        cls.driver.implicitly_wait(10)

    @classmethod
    def tearDownClass(cls):
        cls.driver.quit()
        super().tearDownClass()

    def marianaPercySnapshot(self, name):
        percySnapshot(browser=self.driver, name=name)

    def test_login(self):
        self.driver.get(self.base_url)

        self.driver.find_element_by_id("id_username")
        self.marianaPercySnapshot(name="Login page")

Circle config:

version: 2.1

orbs:
  percy: percy/agent@0.1.3

jobs:
  test:
    docker:
      - image: circleci/python:3.8-node
        environment:
          PIPENV_VENV_IN_PROJECT: true
          SECRET_KEY: secret_key
          DATABASE_URL: postgresql://root@localhost/circle_test?sslmode=disable
      - image: circleci/postgres:9.6.2
        environment:
          POSTGRES_USER: root
          POSTGRES_DB: circle_test
      - image: selenium/standalone-chrome-debug
    steps:
      - attach_workspace:
          at: .
      - checkout

      # handle python packages
      - restore_cache:
          key: pipenv-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
      - run:
          command: |
            sudo pip install pipenv
            pipenv install --dev
      - save_cache:
          key: pipenv-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
          paths:
            - .venv
      - run:
          name: Check formatting of python code
          command: |
            pipenv run black --check --exclude '.venv' .

      # handle tailwind packages
      - restore_cache:
          key: tailwind-{{ .Branch }}-{{ checksum "ui/static_src/package-lock.json" }}
      - run:
          name: Install tailwind
          command: |
            pipenv run python manage.py tailwind install
      - save_cache:
          key: tailwind-{{ .Branch }}-{{ checksum "ui/static_src/package-lock.json" }}
          paths:
            - "ui/static_src/node_modules"

      # build assets
      - run:
          name: Build styles
          command: |
            pipenv run python manage.py tailwind build

      # under the hood percy runs puppeteer which runs a headless chromium which requires
      # all of this stuff
      - run:
          name: Install Chrome headless dependencies
          working_directory: /
          command: |
            sudo apt-get update -y && \
            sudo apt-get upgrade -y && \
            sudo apt-get install -yq \
            gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
            libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libgbm-dev libglib2.0-0 libgtk-3-0 libnspr4 \
            libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \
            libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \
            fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget --fix-missing

      # handle percy set up
      - run:
          name: Install @percy/agent
          command: |
            npm i @percy/agent

      # finally run the tests!
      - run:
          name: Running tests
          command: |
            pipenv run npx percy exec -- \
            python manage.py test

      - store_test_results:
          path: /tmp/circleci-test-results

workflows:
  test:
    jobs:
      - test
      - percy/finalize_all:
          requires:
            - test

Docker config:

version: '3.8'
services:
  postgresql:
    image: postgres:11.6
    environment:
      - POSTGRES_USER=root
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=portal
    restart: always
    ports:
      - 5433:5432
  selenium:
    image: selenium/standalone-chrome-debug:3.141
    ports:
      - 4444:4444
      - 5900:5900

Django settings file:

import os
import sys

import dj_database_url
from decouple import config

TESTING = len(sys.argv) > 1 and sys.argv[1] == "test"

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TEMPLATE_DIR = os.path.join(BASE_DIR, "ui/templates")
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))

SECRET_KEY = config("SECRET_KEY")
DEBUG = config("DEBUG", default=False, cast=bool)
TAILWIND_APP_NAME = "ui"

# Application definition

INSTALLED_APPS = [
    "oauth2_provider",
    "portal",
    "portal.users",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "whitenoise.runserver_nostatic",
    "django_extensions",
    "tailwind",
    "ui",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

AUTH_USER_MODEL = "users.User"

ROOT_URLCONF = "portal.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [TEMPLATE_DIR],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
            "debug": DEBUG,
        },
    }
]

WSGI_APPLICATION = "portal.wsgi.application"

# Database
DATABASES = {
    "default": dj_database_url.parse(
        config("DATABASE_URL"), conn_max_age=600, ssl_require=False
    )
}

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
    },
    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]

# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/

LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = True

# Honor the 'X-Forwarded-Proto' header for request.is_secure()
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

# Allow all host headers
ALLOWED_HOSTS = ["*"]

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/
STATIC_ROOT = os.path.join(BASE_DIR, "ui/static")
STATIC_URL = "/static/"

# Extra places for collectstatic to find static files.
STATICFILES_DIRS = (os.path.join(BASE_DIR, "ui/static_src/src"),)

# Simplified static file serving.
# https://warehouse.python.org/project/whitenoise/
if not TESTING:
    STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

# For local development, don't force the use of SSL on the database connection.
if config("DISABLE_POSTGRES_SSL_MODE", default=False, cast=bool):
    DATABASES["default"].setdefault("OPTIONS", {})["sslmode"] = "disable"

LOGIN_URL = "login"
LOGOUT_URL = "logout"
LOGIN_REDIRECT_URL = "home"

PASSWORD_HASHERS = ("django.contrib.auth.hashers.PBKDF2PasswordHasher",)

# For testing purposes
if DEBUG:
    EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

if config("TEST_OUTPUT_DIR", default=None):
    TEST_RUNNER = "xmlrunner.extra.djangotestrunner.XMLTestRunner"
    TEST_OUTPUT_DIR = config("TEST_OUTPUT_DIR")
    TEST_OUTPUT_VERBOSE = 2
Robdel12 commented 4 years ago

Hey @xiwcx! I replied to your support email earlier today about this too. If you could post the logs from a build (set LOG_LEVEL=debug as an env var) and pass them over we can take a look. I suspect the server is shutting down before the assets are discovered. In the limited logs we do collect from the SDK, it looks like we're getting 404s on some assets:

{"message":"Skipping [disallowed_response_status_404] [1920 px]: http://localhost:8000/static/icons/mt-logo-white.svg | Thu Aug 06 2020 14:38:01 GMT+0000 (Coordinated Universal Time)","level":"debug"}