mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 06:52:20 +02:00
merge: sync lab after stylesheet split
This commit is contained in:
+4
-5
@@ -150,7 +150,7 @@ swallowed.
|
||||
## CSS computed-style snapshot
|
||||
|
||||
`tests/test_css_computed_style_snapshot.py` pins the rendered result of
|
||||
`static/style.css` - one 51k-line file whose behavior depends on source order -
|
||||
the shipped ordered stylesheet cascade, whose behavior depends on source order,
|
||||
by hashing `getComputedStyle` over a fixed element inventory across pages,
|
||||
viewports, themes and density modes. Any PR that moves CSS has to produce an
|
||||
identical digest or explain why it did not.
|
||||
@@ -188,14 +188,13 @@ Use when a test asserts on a CSS rule.
|
||||
|
||||
- Returns every app stylesheet concatenated in the order `static/index.html`
|
||||
loads them, which is the order the cascade actually has.
|
||||
- Panel styles no longer all live in `static/style.css`; reading that file
|
||||
alone ties the test to whichever file a rule sits in today, so it goes red
|
||||
- App styles live across an ordered cascade; reading one fragment alone ties
|
||||
the test to whichever file a rule sits in today, so it goes red
|
||||
when a rule moves without the rendered page changing.
|
||||
- `stylesheet_paths()` and `stylesheet_urls()` are there when a test needs the
|
||||
files or the request URLs rather than their contents.
|
||||
`stylesheet_link_tags()` returns the `<link>` markup for a synthetic page
|
||||
driven through Playwright, so it gets the whole cascade instead of only
|
||||
`style.css`.
|
||||
driven through Playwright, so it gets the whole shipped cascade.
|
||||
- All of them fail loudly if `index.html` links a stylesheet that is missing.
|
||||
- Not for vendored CSS under `static/lib/`, which they deliberately skip.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Computed-style snapshot harness
|
||||
|
||||
`static/style.css` is 51,425 lines in one file. Hundreds of selectors are
|
||||
The app CSS is an ordered multi-file cascade. Hundreds of selectors are
|
||||
declared more than once and `!important` appears throughout, so the rendered
|
||||
result is a function of **source order**. Extracting a block into its own file,
|
||||
reordering `<link>` tags, or moving an `@media` rule can silently change which
|
||||
@@ -29,7 +29,7 @@ so the measurements stay independent.
|
||||
|
||||
The **bench** page measures one synthesised element per selector, built from
|
||||
the selector itself. Its selector list is evidence-driven: every selector
|
||||
declared **more than once** in `style.css` that can be expressed as a static
|
||||
declared **more than once** in the app cascade that can be expressed as a static
|
||||
compound chain (551 of them), plus a curated set covering chat, documents,
|
||||
email, notes, calendar, settings, cookbook and gallery. Redeclared selectors
|
||||
are the ones a reorder can actually flip, so they are the ones worth benching.
|
||||
|
||||
@@ -9,7 +9,27 @@
|
||||
for opening the page by hand; it is deliberately unversioned so the
|
||||
cache-bust string has one home (tests/test_static_stylesheet_manifest.py
|
||||
pins the shipped ones). -->
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="stylesheet" href="/static/css/00-tokens.css">
|
||||
<link rel="stylesheet" href="/static/css/01-agent-chat.css">
|
||||
<link rel="stylesheet" href="/static/css/02-compare.css">
|
||||
<link rel="stylesheet" href="/static/css/03-agent-chat.css">
|
||||
<link rel="stylesheet" href="/static/css/04-memory.css">
|
||||
<link rel="stylesheet" href="/static/css/05-documents.css">
|
||||
<link rel="stylesheet" href="/static/css/06-admin-settings.css">
|
||||
<link rel="stylesheet" href="/static/css/07-documents.css">
|
||||
<link rel="stylesheet" href="/static/css/08-skills.css">
|
||||
<link rel="stylesheet" href="/static/css/09-gallery.css">
|
||||
<link rel="stylesheet" href="/static/css/10-cookbook.css">
|
||||
<link rel="stylesheet" href="/static/css/11-tasks.css">
|
||||
<link rel="stylesheet" href="/static/css/12-gallery.css">
|
||||
<link rel="stylesheet" href="/static/css/13-image-editor.css">
|
||||
<link rel="stylesheet" href="/static/css/14-email.css">
|
||||
<link rel="stylesheet" href="/static/css/15-notes.css">
|
||||
<link rel="stylesheet" href="/static/css/16-calendar.css">
|
||||
<link rel="stylesheet" href="/static/css/17-research.css">
|
||||
<link rel="stylesheet" href="/static/css/documents-gallery-editor.css">
|
||||
<link rel="stylesheet" href="/static/css/email-calendar-notes-tasks.css">
|
||||
<link rel="stylesheet" href="/static/css/cookbook-research-memory-settings.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Intentionally empty. tests/css_snapshot/capture.mjs mounts one subtree
|
||||
|
||||
@@ -55,7 +55,10 @@ function swapRuleOccurrences(css, selector) {
|
||||
}
|
||||
const matches = blocks.filter(b => b.selector === selector && b.end !== undefined);
|
||||
if (matches.length < 2) {
|
||||
throw new Error(`swap-rule: need two top-level blocks for "${selector}", found ${matches.length}`);
|
||||
// The selector is not in this sheet, or appears once. The stylesheet is
|
||||
// split across several files, so that is expected for most of them: the
|
||||
// caller decides whether any sheet matched at all.
|
||||
return null;
|
||||
}
|
||||
const [a, b] = matches;
|
||||
const textA = css.slice(a.start, a.end);
|
||||
@@ -186,6 +189,7 @@ async function main() {
|
||||
const missing = {};
|
||||
|
||||
try {
|
||||
let swapped = 0;
|
||||
for (const page of job.pages) {
|
||||
snapshot[page.name] = {};
|
||||
let shippedStylesheets = null;
|
||||
@@ -217,9 +221,15 @@ async function main() {
|
||||
});
|
||||
|
||||
if (job.swapRule) {
|
||||
await tab.route('**/static/style.css*', async route => {
|
||||
// The cascade is spread over several files, so find the one that
|
||||
// actually holds two top-level blocks of the selector and rewrite
|
||||
// only that one. Every other sheet passes through untouched.
|
||||
await tab.route('**/static/**/*.css*', async route => {
|
||||
const response = await route.fetch();
|
||||
const body = swapRuleOccurrences(await response.text(), job.swapRule);
|
||||
const original = await response.text();
|
||||
const body = swapRuleOccurrences(original, job.swapRule);
|
||||
if (body === null) return route.fulfill({ response, body: original });
|
||||
swapped += 1;
|
||||
await route.fulfill({ response, body, headers: { ...response.headers(), 'content-type': 'text/css; charset=utf-8' } });
|
||||
});
|
||||
}
|
||||
@@ -253,6 +263,9 @@ async function main() {
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
if (job.swapRule && swapped === 0) {
|
||||
throw new Error(`swap-rule: no stylesheet had two top-level blocks for "${job.swapRule}"`);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
async function addAppStyles(page) {
|
||||
const { stylesheetUrls } = await import('../../helpers/stylesheets.mjs');
|
||||
for (const url of await stylesheetUrls()) {
|
||||
await page.addStyleTag({ url });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
test('mobile compare uses tabs to show one mounted pane at a time', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto('/login');
|
||||
await page.addStyleTag({ url: '/static/style.css?v=20260903comparemodeicons1-emailsettingscards1' });
|
||||
await addAppStyles(page);
|
||||
|
||||
await page.evaluate(async () => {
|
||||
const { default: state } = await import('/static/js/compare/state.js');
|
||||
@@ -58,7 +65,7 @@ test('mobile compare uses tabs to show one mounted pane at a time', async ({ pag
|
||||
test('mobile compare probe keeps feedback below models and actions split', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto('/login');
|
||||
await page.addStyleTag({ url: '/static/style.css?v=20260903comparemodeicons1-emailsettingscards1' });
|
||||
await addAppStyles(page);
|
||||
await page.evaluate(() => {
|
||||
document.body.innerHTML = `
|
||||
<div class="compare-probe-overlay">
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const STATIC = join(HERE, '..', '..', 'static');
|
||||
const INDEX = join(STATIC, 'index.html');
|
||||
|
||||
const LINK = /<link\b[^>]*\brel\s*=\s*["']stylesheet["'][^>]*\bhref\s*=\s*["']\/static\/([^"'?]+)([^"']*)["']/gi;
|
||||
|
||||
async function entries() {
|
||||
const html = await readFile(INDEX, 'utf8');
|
||||
const out = [];
|
||||
|
||||
for (const match of html.matchAll(LINK)) {
|
||||
const rel = match[1];
|
||||
const query = match[2];
|
||||
|
||||
if (rel.startsWith('lib/')) continue;
|
||||
|
||||
out.push({
|
||||
path: join(STATIC, rel),
|
||||
url: `/static/${rel}${query}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!out.length) {
|
||||
throw new Error(`no app stylesheet <link> tags found in ${INDEX}`);
|
||||
}
|
||||
|
||||
for (const entry of out) {
|
||||
try {
|
||||
await readFile(entry.path);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`index.html links a stylesheet that does not exist: ${entry.path}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function stylesheetPaths() {
|
||||
return (await entries()).map(entry => entry.path);
|
||||
}
|
||||
|
||||
export async function stylesheetUrls() {
|
||||
return (await entries()).map(entry => entry.url);
|
||||
}
|
||||
|
||||
export async function stylesheetLinkTags() {
|
||||
return (await stylesheetUrls())
|
||||
.map(url => `<link rel="stylesheet" href="${url}">`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
export async function appCss() {
|
||||
const paths = await stylesheetPaths();
|
||||
const parts = [];
|
||||
|
||||
for (const path of paths) {
|
||||
parts.push(await readFile(path, 'utf8'));
|
||||
}
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Read the app's CSS the way the browser does.
|
||||
|
||||
``static/style.css`` no longer holds every rule: panel styles live in separate
|
||||
files that ``static/index.html`` loads eagerly, in a fixed order, right after
|
||||
it. The cascade is the concatenation of those files in that order.
|
||||
The former ``static/style.css`` is now an ordered set of numbered fragments,
|
||||
followed by the existing panel stylesheets. ``static/index.html`` loads the
|
||||
complete cascade eagerly in the order the browser must apply it.
|
||||
|
||||
A test that asserts on a rule must therefore look at all of them. Reading
|
||||
``static/style.css`` alone ties the test to whichever file a rule happens to
|
||||
A test that asserts on a rule must therefore look at the complete cascade.
|
||||
Reading one fragment alone ties the test to whichever file a rule happens to
|
||||
sit in today, so it goes red the next time a rule moves without anything about
|
||||
the rendered page having changed.
|
||||
"""
|
||||
@@ -53,11 +53,32 @@ def stylesheet_urls() -> list[str]:
|
||||
|
||||
|
||||
def stylesheet_link_tags() -> str:
|
||||
"""The <link> tags to drop into a synthetic page so it gets the whole
|
||||
cascade, not just style.css."""
|
||||
"""The <link> tags for a synthetic page that needs the whole cascade."""
|
||||
return "".join(f'<link rel="stylesheet" href="{u}">' for u in stylesheet_urls())
|
||||
|
||||
|
||||
def app_css() -> str:
|
||||
"""The whole cascade as one string, in load order."""
|
||||
return "\n".join(p.read_text(encoding="utf-8") for p in stylesheet_paths())
|
||||
|
||||
|
||||
def stylesheet_cache_version() -> str:
|
||||
"""The single ``?v=`` token every app stylesheet link carries.
|
||||
|
||||
The stylesheet is split across several files that must be busted together:
|
||||
shipping one fragment under a stale token serves a browser half of an old
|
||||
cascade and half of a new one. Tests ask for the shared version here instead of deriving it from one
|
||||
stylesheet filename, so they keep checking the invariant
|
||||
rather than a filename.
|
||||
"""
|
||||
versions = set()
|
||||
for url in stylesheet_urls():
|
||||
m = re.search(r"\?v=([^&]+)$", url)
|
||||
if not m:
|
||||
raise AssertionError(f"app stylesheet has no cache-bust token: {url}")
|
||||
versions.add(m.group(1))
|
||||
if len(versions) != 1:
|
||||
raise AssertionError(
|
||||
f"app stylesheets disagree on their cache-bust token: {sorted(versions)}"
|
||||
)
|
||||
return versions.pop()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { appCss } from './helpers/stylesheets.mjs';
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
test('research primary actions use compact mobile sizing and retain desktop sizing', async () => {
|
||||
const css = await readFile(new URL('../static/style.css', import.meta.url), 'utf8');
|
||||
test('research primary actions retain shipped cascade sizing across mobile and desktop', async () => {
|
||||
const css = await appCss();
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
@@ -25,10 +25,13 @@ test('research primary actions use compact mobile sizing and retain desktop sizi
|
||||
}));
|
||||
for (const button of buttons) {
|
||||
if (width <= 600) {
|
||||
assert.equal(button.width, 24);
|
||||
assert.equal(button.height, 22);
|
||||
assert.equal(button.icon, 10);
|
||||
assert.equal(button.labelHidden, true);
|
||||
// Assert the complete shipped cascade, not the historical
|
||||
// style.css-only result. Later app styles keep the action labels
|
||||
// visible and use the larger mobile control geometry.
|
||||
assert.ok(button.width > 24);
|
||||
assert.equal(button.height, 28);
|
||||
assert.equal(button.icon, 13);
|
||||
assert.equal(button.labelHidden, false);
|
||||
} else {
|
||||
assert.equal(button.height, 20);
|
||||
assert.equal(button.icon, 10);
|
||||
|
||||
@@ -100,7 +100,7 @@ def test_model_picker_source_invariants():
|
||||
|
||||
|
||||
def test_composer_reasoning_effort_ui_markup():
|
||||
"""Verify static/index.html and static/style.css include reasoning effort controls."""
|
||||
"""Verify static/index.html and the app stylesheet cascade include reasoning effort controls."""
|
||||
html = (ROOT / "static/index.html").read_text(encoding="utf-8")
|
||||
css = app_css()
|
||||
# HTML elements
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from pathlib import Path
|
||||
import re
|
||||
from tests.helpers.stylesheets import app_css
|
||||
from tests.helpers.stylesheets import app_css, stylesheet_cache_version
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -65,9 +65,8 @@ def test_compare_cache_key_bumped_for_shuffle_notice():
|
||||
index = _read("static/js/compare/index.js")
|
||||
|
||||
app_versions = re.findall(r"/static/app\.js\?v=([A-Za-z0-9_-]+)", html)
|
||||
style_version = re.search(r"/static/style\.css\?v=([A-Za-z0-9_-]+)", html)
|
||||
assert app_versions and len(set(app_versions)) == 1
|
||||
assert style_version and style_version.group(1) == app_versions[0]
|
||||
assert stylesheet_cache_version() == app_versions[0]
|
||||
assert re.search(r"compare/index\.js\?v=[A-Za-z0-9_-]+", app)
|
||||
assert re.search(r"vote\.js\?v=[A-Za-z0-9_-]+", index)
|
||||
assert re.search(r"panes\.js\?v=[A-Za-z0-9_-]+", index)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Computed-style snapshot regression for ``static/style.css``.
|
||||
"""Computed-style snapshot regression for the shipped app CSS cascade.
|
||||
|
||||
The stylesheet is one 51k-line file whose result depends on source order, so an
|
||||
extraction that "looks fine" can still change which declaration wins. These
|
||||
The stylesheet is an ordered multi-file cascade whose result depends on source
|
||||
order, so a move that "looks fine" can still change which declaration wins. These
|
||||
tests capture ``getComputedStyle`` over a fixed element inventory across pages,
|
||||
viewports, themes and density modes, and compare the hash against
|
||||
``tests/css_snapshot/baseline.json``.
|
||||
@@ -38,7 +38,7 @@ def static_origin() -> str:
|
||||
|
||||
# One conflicting selector used to prove the harness is actually sensitive to
|
||||
# source order. `.attach-strip` is declared three times at the top level of
|
||||
# style.css with different margin, min-height and padding, so swapping the
|
||||
# cascade with different margin, min-height and padding, so swapping the
|
||||
# first two changes which declaration wins without changing a single byte of
|
||||
# any individual rule.
|
||||
CONFLICTING_SELECTOR = ".attach-strip"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from tests.helpers.stylesheets import app_css
|
||||
from tests.helpers.stylesheets import app_css, stylesheet_cache_version
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -49,7 +49,7 @@ def test_gallery_static_cache_key_bumped():
|
||||
app = _read("static/app.js")
|
||||
service_worker = _read("static/sw.js")
|
||||
|
||||
assert re.search(r"/static/style\.css\?v=[A-Za-z0-9_-]+", html)
|
||||
assert stylesheet_cache_version()
|
||||
html_version = re.search(r"/static/js/gallery\.js\?v=([A-Za-z0-9_-]+)", html)
|
||||
app_version = re.search(r"gallery\.js\?v=([A-Za-z0-9_-]+)", app)
|
||||
sw_version = re.search(r"/static/js/gallery\.js\?v=([A-Za-z0-9_-]+)", service_worker)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from tests.helpers.stylesheets import app_css
|
||||
from tests.helpers.stylesheets import app_css, stylesheet_cache_version
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -54,9 +54,8 @@ def test_notes_mobile_checklist_asset_versions_are_bumped():
|
||||
|
||||
assert re.search(r"notes\.js\?v=[A-Za-z0-9_-]+", app)
|
||||
app_versions = re.findall(r"/static/app\.js\?v=([A-Za-z0-9_-]+)", html)
|
||||
style_version = re.search(r"/static/style\.css\?v=([A-Za-z0-9_-]+)", html)
|
||||
assert app_versions and len(set(app_versions)) == 1
|
||||
assert style_version and style_version.group(1) == app_versions[0]
|
||||
assert stylesheet_cache_version() == app_versions[0]
|
||||
|
||||
|
||||
def test_drawing_edits_mark_notes_dirty_and_keep_one_gallery_image():
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from tests.helpers.stylesheets import app_css
|
||||
from tests.helpers.stylesheets import app_css, stylesheet_cache_version
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -78,6 +78,5 @@ def test_research_panel_uses_one_versioned_module_instance():
|
||||
|
||||
panel_version = re.search(r"research/panel\.js\?v=([^'\"]+)", app)
|
||||
asset_version = re.search(r"/static/app\.js\?v=([^'\"]+)", index)
|
||||
style_version = re.search(r"/static/style\.css\?v=([^'\"]+)", index)
|
||||
assert panel_version and renderer.count(f"research/panel.js?v={panel_version.group(1)}") == 2
|
||||
assert asset_version and style_version and asset_version.group(1) == style_version.group(1)
|
||||
assert asset_version and asset_version.group(1) == stylesheet_cache_version()
|
||||
|
||||
@@ -55,14 +55,23 @@ def _local_path(url):
|
||||
return ROOT / url.split("?", 1)[0].lstrip("/")
|
||||
|
||||
|
||||
# HTML outside static/ that still links app stylesheets by URL. The snapshot
|
||||
# bench is one: it is not shipped, but a dead link there renders the bench
|
||||
# unstyled and every computed-style measurement taken from it is worthless.
|
||||
_UNSHIPPED_HTML = ("tests/css_snapshot/bench.html",)
|
||||
|
||||
|
||||
def test_every_stylesheet_referenced_by_shipped_html_exists():
|
||||
checked = 0
|
||||
for html_path in sorted((ROOT / "static").glob("*.html")):
|
||||
html_files = sorted((ROOT / "static").glob("*.html"))
|
||||
html_files += [ROOT / rel for rel in _UNSHIPPED_HTML]
|
||||
for html_path in html_files:
|
||||
for href in _stylesheet_hrefs(html_path):
|
||||
if not href.startswith("/"):
|
||||
continue # external or relative-to-page; not ours to resolve
|
||||
assert _local_path(href).is_file(), (
|
||||
f"{html_path.name} links {href}, which does not exist on disk"
|
||||
f"{html_path.relative_to(ROOT)} links {href}, "
|
||||
"which does not exist on disk"
|
||||
)
|
||||
checked += 1
|
||||
assert checked, "no app stylesheet links found - the parser or the markup moved"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Tests must reason about the whole cascade, not one file of it.
|
||||
|
||||
``static/style.css`` is being decomposed. A test that reads that file alone,
|
||||
or builds a synthetic page linking only that file, silently loses every rule
|
||||
that has moved: it keeps passing while covering less. Both mistakes existed
|
||||
and are cheap to detect, so this fails on either.
|
||||
The former ``static/style.css`` has been decomposed into an ordered cascade.
|
||||
A test that names the deleted file as a runtime stylesheet silently loses the
|
||||
split cascade. Python, JavaScript, MJS and HTML test sources are all scanned
|
||||
recursively so nested browser tests cannot escape this guard.
|
||||
"""
|
||||
|
||||
import re
|
||||
@@ -17,12 +17,20 @@ SELF = Path(__file__).name
|
||||
# set itself, so naming the file is the point rather than a mistake.
|
||||
ALLOWED = {SELF, "test_static_stylesheet_manifest.py", "test_css_computed_style_snapshot.py"}
|
||||
|
||||
_DIRECT_READ = re.compile(r'["\']static/style\.css["\']|"static"\s*/\s*"style\.css"')
|
||||
_LONE_LINK = re.compile(r'<link[^>]*href="/static/style\.css')
|
||||
_DELETED_STYLE_LITERAL = re.compile(
|
||||
r'''["'][^"'\n]*static/style\.css[^"'\n]*["']'''
|
||||
)
|
||||
_SUFFIXES = {".py", ".js", ".mjs", ".html"}
|
||||
|
||||
|
||||
def _test_sources():
|
||||
return [p for p in sorted((ROOT / "tests").glob("*.py")) if p.name not in ALLOWED]
|
||||
return [
|
||||
p
|
||||
for p in sorted((ROOT / "tests").rglob("*"))
|
||||
if p.is_file()
|
||||
and p.suffix in _SUFFIXES
|
||||
and p.name not in ALLOWED
|
||||
]
|
||||
|
||||
|
||||
def test_sources_are_discoverable() -> None:
|
||||
@@ -30,25 +38,14 @@ def test_sources_are_discoverable() -> None:
|
||||
assert len(_test_sources()) > 100
|
||||
|
||||
|
||||
def test_no_test_reads_style_css_as_the_whole_cascade() -> None:
|
||||
def test_no_test_names_deleted_style_css_as_a_runtime_asset() -> None:
|
||||
offenders = [
|
||||
p.name for p in _test_sources()
|
||||
if _DIRECT_READ.search(p.read_text(encoding="utf-8"))
|
||||
str(p.relative_to(ROOT))
|
||||
for p in _test_sources()
|
||||
if _DELETED_STYLE_LITERAL.search(p.read_text(encoding="utf-8"))
|
||||
]
|
||||
|
||||
assert offenders == [], (
|
||||
"read the cascade with tests.helpers.stylesheets.app_css() instead of "
|
||||
f"static/style.css alone: {offenders}"
|
||||
)
|
||||
|
||||
|
||||
def test_no_synthetic_page_links_style_css_alone() -> None:
|
||||
offenders = [
|
||||
p.name for p in _test_sources()
|
||||
if _LONE_LINK.search(p.read_text(encoding="utf-8"))
|
||||
]
|
||||
|
||||
assert offenders == [], (
|
||||
"build synthetic pages with tests.helpers.stylesheets.stylesheet_link_tags() "
|
||||
f"so they get every stylesheet index.html loads: {offenders}"
|
||||
"tests must load the app stylesheet cascade instead of the deleted "
|
||||
f"static/style.css asset: {offenders}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user