tosijs-ui 1.16.36 → 1.16.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.16.37
4
+
5
+ Doc-site test running, as a developer meets it. On a deployed site, tests still start off; the
6
+ only difference there is how a passing run looks to a reader who turns them on.
7
+
8
+ - **Safari no longer stalls the background test run.** The runner loads each page with tests in
9
+ a hidden frame, and parked that frame off-screen; WebKit gives an off-screen frame no
10
+ animation frames, tosijs renders on them, so every page waited out its 45-second deadline and
11
+ the header read "Running 0" for the whole run. The frame now sits inside the viewport, clipped
12
+ so nothing paints. The doc-test gate runs in WebKit for the first time, and passes.
13
+ - **Over the tunnel, tests run as they do on localhost.** They started off there, and turned on
14
+ ran only the page on screen; the dev server now marks the pages it serves, so any page it
15
+ served starts with tests on and runs the whole site in the background. That is every visitor
16
+ to a dev server, on the LAN or through a tunnel link, with or without an edit session: a
17
+ preview link opened on a phone runs every page's tests in hidden frames. Turning tests off
18
+ from the header menu is remembered per browser. A deployed site has no mark and still
19
+ defaults to off.
20
+ - **A pass no longer blanks the header's test widget.** It faded to nothing and left an empty
21
+ rectangle in the bar. It now turns grey and then translucent, all as a CSS filter over the
22
+ pass colour, and stays readable and clickable. Floating over a page with no header it still
23
+ fades out.
24
+
3
25
  ## 1.16.36
4
26
 
5
27
  One additive site option. Nothing changes for a site that does not set it.
@@ -119,6 +119,7 @@ import { popMenu } from './menu.js';
119
119
  import { codeEditor } from './code-editor.js';
120
120
  import { tosiDiff } from './diff.js';
121
121
  import { LAN_REFUSAL_ADVICE } from './live-example/source-access.js';
122
+ import { servedForDevelopment } from './live-example/dev-served.js';
122
123
  /*
123
124
  Does this doc contain a REAL ` ```test ` block, as opposed to mentioning one?
124
125
 
@@ -186,6 +187,18 @@ const testIndicatorStyleSpec = {
186
187
  '70%': { opacity: '1', transform: 'scale(1.1)' },
187
188
  '100%': { opacity: '0', transform: 'scale(0.9)', pointerEvents: 'none' },
188
189
  },
190
+ /*
191
+ A pass in the header settles into a muted, monochrome badge rather than fading out. Faded,
192
+ it left an empty rectangle in the bar (the widget keeps its box), and the result was gone
193
+ for anyone who looked a moment late. Muted, it reads as "nothing to see" and stays a button.
194
+ All of it is a filter over the pass colour, never a new colour. Grey first, then faded: fading
195
+ while still green put a washed-out green on the header's colour.
196
+ */
197
+ '@keyframes test-settle': {
198
+ '0%, 40%': { filter: 'grayscale(0) opacity(1)' },
199
+ '55%': { filter: 'grayscale(1) opacity(1)' },
200
+ '100%': { filter: 'grayscale(1) opacity(0.55)' },
201
+ },
189
202
  // Hide when tests disabled
190
203
  'body:not(.tests-enabled) .doc-link::after, body:not(.tests-enabled) .test-widget': {
191
204
  display: 'none !important',
@@ -240,8 +253,19 @@ const testIndicatorStyleSpec = {
240
253
  '.test-widget.-passed': {
241
254
  _testBg: testColor.pass,
242
255
  _testFg: testTextColor.pass,
256
+ animation: 'test-settle 3s ease-out forwards',
257
+ },
258
+ // Floating over the page (no header), it still gets out of the way: there is no gap to leave.
259
+ '.test-widget.-floating.-passed': {
243
260
  animation: 'test-fade 3s ease-out forwards',
244
261
  },
262
+ '@media (prefers-reduced-motion: reduce)': {
263
+ '.test-widget.-passed': {
264
+ animation: 'none',
265
+ filter: 'grayscale(1) opacity(0.55)',
266
+ },
267
+ '.test-widget.-floating.-passed': { opacity: '0', pointerEvents: 'none' },
268
+ },
245
269
  '.test-widget.-failed': {
246
270
  _testBg: testColor.fail,
247
271
  _testFg: testTextColor.fail,
@@ -1762,7 +1786,7 @@ export function createDocBrowser(options) {
1762
1786
  else {
1763
1787
  testWidget.classList.add('-passed');
1764
1788
  testWidget.classList.remove('-failed');
1765
- testWidget.hidden = false; // Show briefly before fade
1789
+ testWidget.hidden = false; // settles into a muted badge (see `.test-widget.-passed`)
1766
1790
  }
1767
1791
  }
1768
1792
  updateTestWidgetDisplay();
@@ -1938,9 +1962,15 @@ export function createDocBrowser(options) {
1938
1962
  const currentFilename = String(app.currentDoc.filename);
1939
1963
  /*
1940
1964
  Create a hidden iframe that loads the full page. Keep a real layout size — layout-dependent
1941
- example tests need one — but park it off-screen rather than rely on opacity:0 over the
1942
- visible page: Chromium (and haltija on top of it) still composites a 0-opacity layer at 0,0,
1943
- which flashes as the frame navigates page to page.
1965
+ example tests need one — and hide it with a clip that leaves nothing to paint, not opacity:
1966
+ Chromium (and haltija on top of it) still composites a 0-opacity layer at 0,0, which flashes
1967
+ as the frame navigates page to page.
1968
+
1969
+ INSIDE the viewport, though. It used to be parked at left:-10000px, and WebKit throttles a
1970
+ frame outside the viewport: its animation frames never came, tosijs renders on animation
1971
+ frames, so every page sat until its 45s deadline and Safari showed "Running 0" for the whole
1972
+ run. That was the "WebKit's iframe runner never signals" of the e2e lane, and it reached
1973
+ readers once the tunnel ran tests in the background.
1944
1974
 
1945
1975
  THE SIZE IS OVERRIDABLE, because 800x600 was the only width the inline tier had ever run at.
1946
1976
  A doc-system layout regression at phone width shipped in 1.12.3 and was found by a reader,
@@ -1959,7 +1989,7 @@ export function createDocBrowser(options) {
1959
1989
  const vpWidth = Number(vp?.width) > 0 ? Number(vp?.width) : 800;
1960
1990
  const vpHeight = Number(vp?.height) > 0 ? Number(vp?.height) : 600;
1961
1991
  const testFrame = document.createElement('iframe');
1962
- testFrame.style.cssText = `position: fixed; left: -10000px; top: 0; width: ${vpWidth}px; height: ${vpHeight}px; opacity: 0; pointer-events: none;`;
1992
+ testFrame.style.cssText = `position: fixed; left: 0; top: 0; z-index: -1; clip-path: inset(100%); width: ${vpWidth}px; height: ${vpHeight}px; opacity: 0; pointer-events: none;`;
1963
1993
  document.body.appendChild(testFrame);
1964
1994
  // Listen for test results posted from the iframe
1965
1995
  const messageHandler = (event) => {
@@ -2032,11 +2062,12 @@ export function createDocBrowser(options) {
2032
2062
  void settleCurrentPage(currentFilename);
2033
2063
  }
2034
2064
  };
2035
- // Run background tests when enabled (initially or when toggled on)
2065
+ // Run background tests when enabled (initially or when toggled on). Every page where a
2066
+ // developer is looking, the tunnel included; elsewhere only the page on screen.
2036
2067
  const startBackgroundTests = () => {
2037
2068
  if (!testManager.enabled.value)
2038
2069
  return;
2039
- if (isLocalhost) {
2070
+ if (servedForDevelopment()) {
2040
2071
  setTimeout(runBackgroundTests, 1000);
2041
2072
  }
2042
2073
  else {
@@ -31,6 +31,11 @@ import { isLoopbackAddressForAuth as isLoopbackAddress } from './dev-auth.js';
31
31
  *
32
32
  * `root + sep` rather than a bare `startsWith`, or `/srv/docs-evil` counts as inside `/srv/docs`.
33
33
  */
34
+ /**
35
+ * Put `snippet` before every script in `html`: just inside `<head>`, or else just after the
36
+ * doctype. Never before the doctype, which would put the page in quirks mode.
37
+ */
38
+ export declare function injectFirst(html: string, snippet: string): string;
34
39
  export declare function isUnderRoot(root: string, candidate: string): boolean;
35
40
  export declare function resolveWatchPaths(config: {
36
41
  docPaths?: string[];
@@ -28,6 +28,7 @@ import { assertSecretNotServed, ensureSessionSecret, readSessionSecret, sessionS
28
28
  import { revocationList, startRevocationSweep } from './device-revocation.js';
29
29
  import { resolveDataStore, resolveDataFile, dataContentProblem, listDataFiles, saveDataFile, dataHistory, readDataAt, isGitWorkTree, } from './data-store.js';
30
30
  import { SOURCE_ACCESS_HEADER, SOURCE_ENDPOINT_SNIPPET, } from '../../live-example/source-access.js';
31
+ import { DEV_SERVER_SNIPPET } from '../../live-example/dev-served.js';
31
32
  /**
32
33
  * Every path the dev server watches for changes.
33
34
  *
@@ -59,6 +60,20 @@ import { SOURCE_ACCESS_HEADER, SOURCE_ENDPOINT_SNIPPET, } from '../../live-examp
59
60
  *
60
61
  * `root + sep` rather than a bare `startsWith`, or `/srv/docs-evil` counts as inside `/srv/docs`.
61
62
  */
63
+ /**
64
+ * Put `snippet` before every script in `html`: just inside `<head>`, or else just after the
65
+ * doctype. Never before the doctype, which would put the page in quirks mode.
66
+ */
67
+ export function injectFirst(html, snippet) {
68
+ const head = /<head(\s[^>]*)?>/i.exec(html);
69
+ if (head) {
70
+ const at = head.index + head[0].length;
71
+ return html.slice(0, at) + snippet + html.slice(at);
72
+ }
73
+ const doctype = /^\s*<!doctype[^>]*>/i.exec(html);
74
+ const at = doctype ? doctype[0].length : 0;
75
+ return html.slice(0, at) + snippet + html.slice(at);
76
+ }
62
77
  export function isUnderRoot(root, candidate) {
63
78
  const base = path.resolve(root);
64
79
  const target = path.resolve(candidate);
@@ -1373,12 +1388,19 @@ export async function devServer(config, opts = {}) {
1373
1388
  // deployed static site, which has none, is never asked.
1374
1389
  (config.editableSources ? SOURCE_ENDPOINT_SNIPPET : '');
1375
1390
  const encoding = negotiateEncoding(request?.headers.get('accept-encoding') ?? null);
1376
- if ((extras || asRoot) && filePath.endsWith('.html')) {
1391
+ if (filePath.endsWith('.html')) {
1377
1392
  const read = await Bun.file(filePath).text();
1378
1393
  const html = asRoot ? rootRelativeUrls(read) : read;
1379
- const injected = html.includes('</body>')
1394
+ const withExtras = html.includes('</body>')
1380
1395
  ? html.replace('</body>', `${extras}</body>`)
1381
1396
  : html + extras;
1397
+ /*
1398
+ Tests on by default, every page, for whoever is looking through the dev server (the tunnel
1399
+ included); see dev-served.ts. FIRST in <head>, unlike the extras: it is read when the
1400
+ bundle evaluates, and on the iife path that is a classic script, which runs as it is
1401
+ parsed, before anything at the end of <body> exists.
1402
+ */
1403
+ const injected = injectFirst(withExtras, DEV_SERVER_SNIPPET);
1382
1404
  // Injected HTML is built per request, so it is compressed but never cached — the
1383
1405
  // cache key would have to include the injected extras, and HTML is small anyway.
1384
1406
  const body = encoding