Files
Anthias/tests/_seed_data.py
Viktor Petersson 5f26e4c42e feat(tests): marketing screenshot testbed via integration suite (#2895)
* feat(tests): marketing screenshot testbed via integration suite

Unifies marketing-asset generation with the existing Playwright
integration tests rather than forking a parallel suite. Setting
MARKETING_SCREENSHOTS=1 turns the new ``marketing_screenshot`` fixture
in tests/conftest.py from a no-op into a capture step that emits
@1x/@2x/@3x sibling PNGs under test-artifacts/marketing/ — matching
the website's existing overview*.png convention.

Seed data moves to tests/_seed_data.py so test_app.py and
test_migrate_to_screenly.py render the same Office Space parody
content (Initech / TPS / Lumbergh / Chotchkie's / Milton). Two new
integration tests — test_home_renders_with_full_schedule and
test_add_asset_modal_layers_over_full_schedule — exercise a populated
asset table that the per-row smoke tests miss, and double as the
``home`` and ``add-asset`` marketing captures.

The new .github/workflows/marketing-screenshots.yaml is
workflow_dispatch only, re-runs the integration suite with the env
var set, and uploads test-artifacts/marketing/ as a 90-day artifact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style: ruff format pass on tests/conftest.py

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(tests): address Copilot review on marketing-screenshot PR

- _seed_data.py: clarify that the module-level seed singletons capture
  their schedule window at import time (only home_seed_assets() is a
  factory); fix a stale comment that referenced the old
  WIZARD_LOCAL_VIDEO_BASENAME name.
- conftest.py + workflow: docstrings + section comment now describe
  the actual artefact naming (<name>.png + <name>@2x.png +
  <name>@3x.png) instead of an inaccurate "@1x/@2x/@3x" shorthand.
  Matches the existing website/assets/images/overview*.png convention,
  which is what Hugo's image-set picker resolves.
- test_app.py: the two new tests now assert concrete layout properties
  (each row's name cell has nonzero width; the add-asset modal card
  has a real bounding box inside the viewport) so a layout regression
  fails CI without requiring MARKETING_SCREENSHOTS=1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(tests): address Copilot review round 2 on PR #2895

- _seed_data.py: home_seed_assets() now wraps the module-level
  singletons through a new _with_fresh_window() helper so every dict
  in the returned list carries a window computed at call time. Honours
  the "fresh schedule on each call" contract the docstring promises;
  a time-travelling test no longer gets the import-time freeze for
  the first / second / last rows.
- test_app.py: test_home_renders_with_full_schedule now asserts each
  row's right edge stays inside the viewport AND that the rightmost
  action button (Delete) is visible + clickable — a layout regression
  that pushes the action cluster off-screen now fails CI instead of
  being caught only by the marketing capture.
- test_app.py: test_add_asset_modal_layers_over_full_schedule now
  calls document.elementFromPoint() at the modal centre and asserts
  the topmost element lives inside .modal-card. A z-index regression
  that leaves an asset row floating above the modal is now caught at
  the assertion layer instead of relying on a visual diff.
- marketing-screenshots.yaml: the pytest step is wrapped in
  nick-fields/retry@v4 (timeout 8 min, 3 attempts) to mirror the
  existing test-runner.yml integration step. A transient Playwright /
  SQLite WAL flake no longer aborts the manual artifact pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(tests): address Copilot review round 3 on PR #2895

- _seed_data.py: docstring no longer claims the parody avoids
  registered/copyrighted material — every name (Initech, Lumbergh,
  Chotchkie's, Milton, Peter Gibbons) is an Office Space reference.
  Now describes the content honestly as parody/fair-use of character
  and company names; the file remains the single swap point for
  downstream uses that need fully generic naming.
- test_migrate_to_screenly.py: the failure-flow test no longer
  hardcodes abc1.mp4. Both the mocked backend error and the assertion
  reuse WIZARD_VIDEO_BASENAME, so a future seed rename only has to
  touch one place. The wizard test's docstring also points at the
  constant instead of the stale literal.
- test_app.py (home): viewport-bound assertion on the Delete button
  now uses the same +1px tolerance as the row-edge check — Playwright
  reports floating-point CSS pixels and the strict comparison flaked
  intermittently under the 3× marketing device scale.
- test_app.py (modal): wait for Element.getAnimations({subtree:true})
  to settle before asserting the modal card's bbox. The .modal-card
  has a 220ms modal-in keyframe animation; without explicitly waiting,
  the marketing screenshot can land mid-fade. Same +1px tolerance
  applied to the modal viewport-bounds check.
- marketing-screenshots.yaml: explicit `permissions: contents: read`
  block so a workflow_dispatch run against an arbitrary ref cannot
  inherit broader repo write scopes. The retry command now clears
  test-artifacts/marketing before each attempt so an earlier
  partial-success can't stamp stale screenshots onto the artifact
  upload if a later attempt aborts before the capture tests run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(tests): address Copilot review round 4 on PR #2895

- conftest.py: new session-scoped autouse _reset_marketing_dir
  fixture clears test-artifacts/marketing/ from inside the container
  when MARKETING_SCREENSHOTS=1. Replaces the workflow's host-side
  rm -rf, which failed with permission-denied on a retry because the
  bind-mounted artefact dir is owned by the root-running anthias-test
  container. Verified locally: a STALE.png planted before the run is
  removed before any test executes, then all 9 fresh captures land.
- test_app.py (home): adds an explicit .drag-handle visibility +
  width check on each enabled row. The "drag handle stays reachable"
  promise in the docstring is now actually asserted, not just
  implied by the row/Delete-button checks.
- test_migrate_to_screenly.py: the mocked migration failure on asset
  a2 (the URL-backed webpage seed) now uses a URL-fetch error string
  referencing WIZARD_WEBPAGE_URL — the previous "File not found on
  device" wording with WIZARD_VIDEO_BASENAME was internally
  inconsistent (would only make sense for a1/a3, the local-file
  seeds) and could hide a row-association mistake.
- marketing-screenshots.yaml: drops the host-side rm -rf
  test-artifacts/marketing (now handled inside the container by the
  fixture above), updates the comment to point at the fixture.

PR body also updated to describe the artifact set precisely
(<name>.png + <name>@2x.png + <name>@3x.png — no @1x suffix on the
base image; matches the existing website overview*.png convention).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:19:51 +01:00

204 lines
7.0 KiB
Python

"""Office Space-themed sample assets shared by the integration suite.
A single source of truth so test_app.py, test_migrate_to_screenly.py
and any future Playwright test render the same brand-consistent
content. When ``MARKETING_SCREENSHOTS=1`` is set the same data lights
up high-DPI captures via the ``marketing_screenshot`` fixture in
``tests/conftest.py``; in the default integration run the seeds are
just the data the tests work against, no different from the bare
dicts they replaced.
Sample content is a parody nod to the 1999 film "Office Space"
Initech, Lumbergh, Chotchkie's, Milton, Peter Gibbons. No movie
stills, logos or assets are bundled; URIs point at ``example.com``
and made-up local paths. The parody is recognisable on purpose so
the marketing captures read as "real digital signage" rather than
generic placeholder text, and falls within standard parody/fair-use
treatment for character and company names. If a downstream use
requires fully generic naming (e.g. for paid ads), the constants
below are the single place to swap names. ``asset_id`` values match
the originals so tests that key off them (e.g. the migration
wizard's per-asset call-log assertion) keep working unchanged.
"""
from __future__ import annotations
from datetime import timedelta
from typing import Any
from django.utils import timezone
def _now_window() -> dict[str, Any]:
"""Yesterday → tomorrow so every seed renders on the home page.
Module-level seed constants below call ``_seed()`` (and therefore
``_now_window()``) at import time, so their ``start_date`` /
``end_date`` are captured against wall-clock at the moment this
module first loads. ``home_seed_assets()`` is a function and
re-evaluates the window on each call. No current test combines
these singletons with ``time_machine``; a test that needs to
travel time should construct fresh seeds via ``_seed()`` or use
the factory rather than the singletons."""
return {
'start_date': timezone.now() - timedelta(days=1),
'end_date': timezone.now() + timedelta(days=1),
}
def _seed(
*,
asset_id: str,
name: str,
mimetype: str,
uri: str,
duration: int = 8,
is_enabled: int = 1,
play_order: int = 0,
) -> dict[str, Any]:
return {
**_now_window(),
'asset_id': asset_id,
'name': name,
'mimetype': mimetype,
'uri': uri,
'duration': duration,
'is_enabled': is_enabled,
'nocache': 0,
'play_order': play_order,
'skip_asset_check': 0,
}
# ---------------------------------------------------------------------------
# Home-page singletons
#
# ``asset_id`` values are preserved verbatim from the pre-rename dicts
# in test_app.py so any per-id assertion keeps matching. Mimetype short
# codes ('image' / 'web') likewise match the originals — the URL-form
# handler stores 'webpage', but several smoke tests fixture rows in
# directly with 'web', and changing that would shift behaviour beyond
# the rename.
# ---------------------------------------------------------------------------
INITECH_ANNOUNCEMENT = _seed(
asset_id='7e978f8c1204a6f70770a1eb54a76e9b',
name='Initech — Q4 All-Hands Recap',
mimetype='image',
uri='https://example.com/initech/q4-allhands-recap.png',
duration=6,
play_order=0,
)
LUMBERGH_MEMO = _seed(
asset_id='4c8dbce552edb5812d3a866cfe5f159d',
name='Memo: TPS Cover Sheet — Effective Immediately',
mimetype='web',
uri='https://example.com/initech/tps-coversheet-memo',
duration=10,
play_order=1,
)
CHOTCHKIES_FLAIR_POLICY = _seed(
asset_id='aa11bb22cc33dd44ee55ff6677889900',
name="Chotchkie's — Pieces of Flair Policy",
mimetype='web',
uri='https://example.com/chotchkies/flair-policy',
duration=5,
is_enabled=0,
play_order=99,
)
def _with_fresh_window(seed: dict[str, Any]) -> dict[str, Any]:
"""Return a copy of ``seed`` with ``start_date`` / ``end_date``
recomputed against current wall-clock. The module-level seed
singletons freeze their window at import time; this helper lets
factories that promise "fresh schedule" honour that contract
without redefining every field."""
return {**seed, **_now_window()}
def home_seed_assets() -> list[dict[str, Any]]:
"""A representative 6-asset schedule for marketing-quality home
captures and the ``test_home_renders_with_full_schedule`` layout-
regression test. Mix of mimetypes, durations and an explicit
disabled row so the table renders every visual branch in one go.
Every dict in the returned list has its schedule window computed
on each call — the singletons are wrapped through
``_with_fresh_window`` so a time-travelling test sees an
in-window schedule rather than the values captured at import."""
return [
_with_fresh_window(INITECH_ANNOUNCEMENT),
_with_fresh_window(LUMBERGH_MEMO),
_seed(
asset_id='b1d31a8f2e7c4d5a9f6b2e1c8d3f0a4e',
name='Milton — Stapler Inventory Audit',
mimetype='image',
uri='/data/anthias_assets/stapler-audit.png',
duration=5,
play_order=2,
),
_seed(
asset_id='c2e42b9f3d8e5a6b0c7d3f2a9b4e1d5f',
name='Office Olympics — Sign-up Open',
mimetype='web',
uri='https://example.com/initech/olympics',
duration=12,
play_order=3,
),
_seed(
asset_id='d3f53cad4e9f6b7c1d8e4a3b0c5f2e6a',
name="Chotchkie's — Tuesday Lunch Special",
mimetype='image',
uri='/data/anthias_assets/chotchkies-tuesday.png',
duration=7,
play_order=4,
),
_with_fresh_window(CHOTCHKIES_FLAIR_POLICY),
]
# ---------------------------------------------------------------------------
# Migration-wizard seeds (image + video + URL-backed webpage)
#
# Identifiers a1/a2/a3 are referenced directly by the wizard test's
# call_log assertions ('a1': 1, 'a2': 2, 'a3': 1 etc.), so they stay
# short. ``WIZARD_VIDEO_BASENAME`` / ``WIZARD_IMAGE_BASENAME`` /
# ``WIZARD_WEBPAGE_URL`` are exported as named constants so the
# wizard test's "displayUri strips /data/anthias_assets/ prefix"
# assertions don't have to hardcode the literal again.
# ---------------------------------------------------------------------------
WIZARD_VIDEO_BASENAME = 'initech-allhands-q4.mp4'
WIZARD_IMAGE_BASENAME = 'welcome-pgibbons.png'
WIZARD_WEBPAGE_URL = 'https://example.com/initech/conf-b'
WIZARD_VIDEO = _seed(
asset_id='a1',
name='Quarterly All-Hands — Q4',
mimetype='video',
uri=f'/data/anthias_assets/{WIZARD_VIDEO_BASENAME}',
)
WIZARD_WEBPAGE = _seed(
asset_id='a2',
name='Conference Room B — Today',
mimetype='webpage',
uri=WIZARD_WEBPAGE_URL,
)
WIZARD_IMAGE = _seed(
asset_id='a3',
name='Welcome — New Hire: Peter Gibbons',
mimetype='image',
uri=f'/data/anthias_assets/{WIZARD_IMAGE_BASENAME}',
)
WIZARD_SEED_ASSETS = [WIZARD_VIDEO, WIZARD_WEBPAGE, WIZARD_IMAGE]