dsh-better-display 0.3.7 → 0.3.9
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 +22 -1
- package/DESIGN.md +43 -3
- package/README.en.md +9 -1
- package/README.md +9 -1
- package/docs/dependency-update-policy.md +9 -0
- package/docs/extension-points.md +172 -0
- package/docs/official-rendering-bridge.md +59 -18
- package/docs/openapi.yaml +147 -0
- package/docs/runbooks/blank-reader.md +39 -0
- package/lib/client.js +1629 -586
- package/lib/client.js.map +1 -1
- package/lib/dsh-better-display.js +535 -57
- package/lib/types/client/AssistantNode.d.ts +20 -0
- package/lib/types/client/AssistantNode.d.ts.map +1 -0
- package/lib/types/client/AssistantNode.js +46 -0
- package/lib/types/client/AssistantNode.js.map +1 -0
- package/lib/types/client/Blocks.d.ts.map +1 -1
- package/lib/types/client/Blocks.js +33 -10
- package/lib/types/client/Blocks.js.map +1 -1
- package/lib/types/client/ChoreographedFlow.d.ts.map +1 -1
- package/lib/types/client/ChoreographedFlow.js +2 -0
- package/lib/types/client/ChoreographedFlow.js.map +1 -1
- package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -1
- package/lib/types/client/ClosedProcessSummary.js +3 -1
- package/lib/types/client/ClosedProcessSummary.js.map +1 -1
- package/lib/types/client/McpAppFrame.d.ts.map +1 -1
- package/lib/types/client/McpAppFrame.js +5 -11
- package/lib/types/client/McpAppFrame.js.map +1 -1
- package/lib/types/client/OfficialContent.d.ts +17 -3
- package/lib/types/client/OfficialContent.d.ts.map +1 -1
- package/lib/types/client/OfficialContent.js +33 -7
- package/lib/types/client/OfficialContent.js.map +1 -1
- package/lib/types/client/Reader.d.ts.map +1 -1
- package/lib/types/client/Reader.js +76 -73
- package/lib/types/client/Reader.js.map +1 -1
- package/lib/types/client/SettingsSection.d.ts.map +1 -1
- package/lib/types/client/SettingsSection.js +0 -5
- package/lib/types/client/SettingsSection.js.map +1 -1
- package/lib/types/client/StickyLane.d.ts +12 -0
- package/lib/types/client/StickyLane.d.ts.map +1 -1
- package/lib/types/client/StickyLane.js +78 -2
- package/lib/types/client/StickyLane.js.map +1 -1
- package/lib/types/client/ToolActivity.d.ts.map +1 -1
- package/lib/types/client/ToolActivity.js +10 -5
- package/lib/types/client/ToolActivity.js.map +1 -1
- package/lib/types/client/deliverables.d.ts.map +1 -1
- package/lib/types/client/deliverables.js +2 -0
- package/lib/types/client/deliverables.js.map +1 -1
- package/lib/types/client/index.d.ts +3 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +15 -1
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/client/live-turn.d.ts +17 -5
- package/lib/types/client/live-turn.d.ts.map +1 -1
- package/lib/types/client/live-turn.js +106 -76
- package/lib/types/client/live-turn.js.map +1 -1
- package/lib/types/client/motion.d.ts +26 -2
- package/lib/types/client/motion.d.ts.map +1 -1
- package/lib/types/client/motion.js +144 -10
- package/lib/types/client/motion.js.map +1 -1
- package/lib/types/client/official-slots.d.ts +9 -0
- package/lib/types/client/official-slots.d.ts.map +1 -1
- package/lib/types/client/official-slots.js +22 -4
- package/lib/types/client/official-slots.js.map +1 -1
- package/lib/types/client/open-file.d.ts.map +1 -1
- package/lib/types/client/open-file.js +1 -6
- package/lib/types/client/open-file.js.map +1 -1
- package/lib/types/client/projection.d.ts +15 -2
- package/lib/types/client/projection.d.ts.map +1 -1
- package/lib/types/client/projection.js +16 -4
- package/lib/types/client/projection.js.map +1 -1
- package/lib/types/client/rewind-cut.d.ts +131 -0
- package/lib/types/client/rewind-cut.d.ts.map +1 -0
- package/lib/types/client/rewind-cut.js +321 -0
- package/lib/types/client/rewind-cut.js.map +1 -0
- package/lib/types/client/styles.d.ts +3 -0
- package/lib/types/client/styles.d.ts.map +1 -0
- package/lib/types/client/styles.js +25 -0
- package/lib/types/client/styles.js.map +1 -0
- package/lib/types/client/text-transform.d.ts +93 -0
- package/lib/types/client/text-transform.d.ts.map +1 -0
- package/lib/types/client/text-transform.js +183 -0
- package/lib/types/client/text-transform.js.map +1 -0
- package/lib/types/client/timeline.d.ts.map +1 -1
- package/lib/types/client/timeline.js.map +1 -1
- package/lib/types/client/tool-activity.d.ts +11 -4
- package/lib/types/client/tool-activity.d.ts.map +1 -1
- package/lib/types/client/tool-activity.js +54 -9
- package/lib/types/client/tool-activity.js.map +1 -1
- package/lib/types/client/turn-slots.d.ts +29 -0
- package/lib/types/client/turn-slots.d.ts.map +1 -0
- package/lib/types/client/turn-slots.js +62 -0
- package/lib/types/client/turn-slots.js.map +1 -0
- package/lib/types/client/types.d.ts +18 -0
- package/lib/types/client/types.d.ts.map +1 -1
- package/lib/types/dsh-better-display.d.ts +4 -1
- package/lib/types/dsh-better-display.d.ts.map +1 -1
- package/lib/types/dsh-better-display.js +55 -43
- package/lib/types/dsh-better-display.js.map +1 -1
- package/lib/types/flags.d.ts +7 -0
- package/lib/types/flags.d.ts.map +1 -0
- package/lib/types/flags.js +37 -0
- package/lib/types/flags.js.map +1 -0
- package/lib/types/log.d.ts +17 -0
- package/lib/types/log.d.ts.map +1 -0
- package/lib/types/log.js +122 -0
- package/lib/types/log.js.map +1 -0
- package/lib/types/metrics.d.ts +18 -0
- package/lib/types/metrics.d.ts.map +1 -0
- package/lib/types/metrics.js +35 -0
- package/lib/types/metrics.js.map +1 -0
- package/lib/types/reveal.d.ts +12 -0
- package/lib/types/reveal.d.ts.map +1 -0
- package/lib/types/reveal.js +147 -0
- package/lib/types/reveal.js.map +1 -0
- package/lib/types/workspace-path.d.ts +3 -0
- package/lib/types/workspace-path.d.ts.map +1 -0
- package/lib/types/workspace-path.js +5 -0
- package/lib/types/workspace-path.js.map +1 -0
- package/package.json +36 -5
- package/scripts/changelog.mjs +20 -0
- package/scripts/check-agents-md.mjs +67 -0
- package/scripts/check-bundle-size.mjs +38 -0
- package/scripts/check-dead-flags.mjs +55 -0
- package/scripts/check-todo-debt.mjs +69 -0
- package/scripts/security-audit-report.mjs +92 -0
- package/scripts/verify-commentary-visible.mjs +627 -0
- package/src/client/AssistantNode.tsx +72 -0
- package/src/client/Blocks.tsx +49 -10
- package/src/client/ChoreographedFlow.tsx +3 -1
- package/src/client/ClosedProcessSummary.tsx +3 -1
- package/src/client/McpAppFrame.tsx +5 -13
- package/src/client/OfficialContent.tsx +47 -9
- package/src/client/Reader.module.css +103 -21
- package/src/client/Reader.tsx +83 -88
- package/src/client/SettingsSection.tsx +1 -7
- package/src/client/StickyLane.tsx +81 -2
- package/src/client/ToolActivity.tsx +10 -6
- package/src/client/deliverables.ts +1 -0
- package/src/client/index.tsx +18 -2
- package/src/client/live-turn.ts +100 -69
- package/src/client/motion.tsx +142 -7
- package/src/client/official-slots.tsx +23 -4
- package/src/client/open-file.ts +2 -8
- package/src/client/projection.ts +16 -4
- package/src/client/rewind-cut.ts +340 -0
- package/src/client/styles.ts +22 -0
- package/src/client/text-transform.ts +265 -0
- package/src/client/timeline.ts +0 -2
- package/src/client/tool-activity.ts +67 -9
- package/src/client/turn-slots.ts +80 -0
- package/src/client/types.ts +19 -0
- package/src/dsh-better-display.ts +57 -42
- package/src/flags.ts +40 -0
- package/src/log.ts +137 -0
- package/src/metrics.ts +50 -0
- package/src/reveal.ts +90 -0
- package/src/workspace-path.ts +4 -0
- package/tests/browser-commentary.tsx +191 -0
- package/tests/chat-flow-hook.test.ts +1 -1
- package/tests/cloud-gate.test.ts +1 -1
- package/tests/context-tool-update.test.ts +1 -1
- package/tests/conversation-scroll.test.ts +1 -1
- package/tests/deliverables.test.ts +1 -1
- package/tests/entry.test.ts +1 -1
- package/tests/fixtures/reader-lane-owners.tsx +46 -0
- package/tests/fixtures/reader-official-users.tsx +234 -0
- package/tests/fixtures/reader-theme-overlap.html +80 -0
- package/tests/fixtures/reader-user-bubble.tsx +222 -0
- package/tests/flags.test.ts +66 -0
- package/tests/fold-choreography.test.ts +32 -8
- package/tests/fold-intensity.test.ts +11 -6
- package/tests/footer-precedence.test.ts +9 -1
- package/tests/history-word-regression.test.ts +1 -1
- package/tests/live-turn.test.ts +125 -54
- package/tests/log.test.ts +99 -0
- package/tests/mcp-apps.test.ts +1 -1
- package/tests/message-chrome.test.ts +1 -1
- package/tests/metrics.test.ts +52 -0
- package/tests/official-slots.test.ts +41 -2
- package/tests/open-file.test.ts +1 -1
- package/tests/pending-submission.test.ts +12 -1
- package/tests/pinned-selection.test.ts +16 -0
- package/tests/platform-media.test.ts +1 -1
- package/tests/projection.test.ts +66 -3
- package/tests/reader-glass-css.test.ts +28 -0
- package/tests/reader-official-users.test.ts +91 -0
- package/tests/reader-sticky-surface.test.ts +377 -0
- package/tests/reader-theme-lanes.test.ts +54 -0
- package/tests/reader-theme-overlap.test.ts +151 -0
- package/tests/reader-user-bubble.test.ts +137 -0
- package/tests/reasoning-follow.test.ts +1 -1
- package/tests/retiring-content.test.ts +229 -0
- package/tests/reveal.test.ts +128 -0
- package/tests/rewind-cut.test.ts +1090 -0
- package/tests/settings-copy.test.ts +1 -1
- package/tests/skill-status.test.ts +1 -1
- package/tests/stock-install.test.ts +9 -1
- package/tests/stream-buffer.test.ts +1 -1
- package/tests/styles.test.ts +75 -0
- package/tests/terminal-input-preview.test.ts +113 -0
- package/tests/text-transform.test.ts +272 -0
- package/tests/timeline.test.ts +1 -1
- package/tests/tool-clock.test.ts +1 -1
- package/tests/tool-diff.test.ts +1 -1
- package/tests/waiting-clock.test.ts +1 -1
- package/tests/word-timeline.test.ts +1 -1
- package/tsdown.config.ts +49 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { test } from 'vitest';
|
|
4
|
+
import { externalClientBundle } from '../tools/client-build.js';
|
|
5
|
+
|
|
6
|
+
// Exercise the actual build plugin so options cannot drift from production.
|
|
7
|
+
async function readerCss(): Promise<string> {
|
|
8
|
+
const configs = externalClientBundle('dsh-better-display', [], { clientEntry: 'src/client/index.tsx' });
|
|
9
|
+
const client = configs.find(config => config.name === 'dsh-better-display/client');
|
|
10
|
+
const plugin = client?.plugins.find(plugin => plugin.name === 'dshx-css-modules-inline');
|
|
11
|
+
assert.ok(plugin);
|
|
12
|
+
const source = await plugin.load.call({}, `\0dshx-css-module:${resolve('src/client/Reader.module.css')}.mjs`);
|
|
13
|
+
const literal = /^const css = (.+);$/m.exec(source)?.[1];
|
|
14
|
+
assert.ok(literal, 'the build emits an inline CSS string');
|
|
15
|
+
const css: unknown = JSON.parse(literal);
|
|
16
|
+
assert.equal(typeof css, 'string');
|
|
17
|
+
if (typeof css !== 'string') throw new Error('Expected CSS text');
|
|
18
|
+
return css;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('built Reader CSS retains Chromium blur and the older Safari fallback', async () => {
|
|
22
|
+
const css = await readerCss();
|
|
23
|
+
assert.match(css, /(?<![\w-])backdrop-filter:blur\(12px\)/);
|
|
24
|
+
assert.match(css, /-webkit-backdrop-filter:blur\(12px\)/);
|
|
25
|
+
const prefixed = [...css.matchAll(/-webkit-backdrop-filter:([^;}]+)/g)].map(match => match[1]);
|
|
26
|
+
const standard = [...css.matchAll(/(?<![\w-])backdrop-filter:([^;}]+)/g)].map(match => match[1]);
|
|
27
|
+
assert.deepEqual(standard, prefixed, 'every Safari fallback also has a working standard declaration');
|
|
28
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { homedir, tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
import { test } from 'vitest';
|
|
9
|
+
|
|
10
|
+
// Needs the Harness source tree and a local pinned Chromium; CI (which builds the
|
|
11
|
+
// release) has neither browser runtime, so the test skips there instead of failing.
|
|
12
|
+
const harnessPointer = join(homedir(), '.config/dshx/harness');
|
|
13
|
+
const harness = process.env.DSHX_HARNESS?.trim()
|
|
14
|
+
|| (existsSync(harnessPointer) ? readFileSync(harnessPointer, 'utf8').trim() : '');
|
|
15
|
+
const browserRuntime = join(homedir(), '.codex/playwright-runtime/runtime.mjs');
|
|
16
|
+
const skip = !harness ? 'DSH Harness source not available'
|
|
17
|
+
: !existsSync(browserRuntime) ? 'pinned Chromium runtime not available' : false;
|
|
18
|
+
|
|
19
|
+
test('official user output stays inside the cluster that carries the host width', () => {
|
|
20
|
+
const root = resolve(import.meta.dirname, '..');
|
|
21
|
+
const source = readFileSync(join(root, 'src/client/Reader.tsx'), 'utf8');
|
|
22
|
+
const css = readFileSync(join(root, 'src/client/Reader.module.css'), 'utf8');
|
|
23
|
+
assert.match(source, /className=\{css\.userCluster\}[^>]*data-reader-key=\{nodeKey\}[^>]*>\{rendered\}/);
|
|
24
|
+
assert.match(css, /\.userCluster\s*\{[^}]*width:\s*100%/);
|
|
25
|
+
assert.match(css, /\.userCluster > \[data-slot\] > \* \{[^}]*width:\s*100%/);
|
|
26
|
+
assert.doesNotMatch(css, /\[data-slot="conversation\.chat\.node"\]/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('headless Reader users retain official renderers, anchors and empty-key fallback', { skip }, async () => {
|
|
30
|
+
const root = resolve(import.meta.dirname, '..');
|
|
31
|
+
const require = createRequire(join(root, 'package.json'));
|
|
32
|
+
const { build } = createRequire(require.resolve('tsx'))('esbuild');
|
|
33
|
+
const webRequire = createRequire(join(harness, 'packages/client/web/package.json'));
|
|
34
|
+
const out = mkdtempSync(join(tmpdir(), 'reader-official-users-'));
|
|
35
|
+
try {
|
|
36
|
+
const { launchPinnedChromium } = await import(pathToFileURL(browserRuntime).href);
|
|
37
|
+
await build({
|
|
38
|
+
entryPoints: [join(root, 'tests/fixtures/reader-official-users.tsx')],
|
|
39
|
+
outfile: join(out, 'fixture.js'), bundle: true, platform: 'browser', format: 'esm', jsx: 'automatic',
|
|
40
|
+
alias: {
|
|
41
|
+
'@fixture/registry': join(harness, 'packages/client/ui-renderer/src/client/registry.ts'),
|
|
42
|
+
'@fixture/renderer': join(harness, 'packages/client/ui-renderer/src/client/scoped-slots.tsx'),
|
|
43
|
+
'@deepseek-ai/dsh-client-ui-primitives': join(harness, 'packages/client/ui-primitives/src/index.ts'),
|
|
44
|
+
'@deepseek-ai/dsh-client-ui-slots': join(harness, 'packages/client/ui-slots/src/index.ts'),
|
|
45
|
+
// The published store lib leaves zustand/immer to the host; the source
|
|
46
|
+
// package resolves them from the Harness workspace.
|
|
47
|
+
'@deepseek-ai/dsh-client-store': join(harness, 'packages/client/store/src/index.ts'),
|
|
48
|
+
react: dirname(require.resolve('react/package.json')),
|
|
49
|
+
'react-dom': dirname(webRequire.resolve('react-dom/package.json')),
|
|
50
|
+
},
|
|
51
|
+
loader: { '.css': 'empty', '.woff2': 'empty', '.woff': 'empty', '.ttf': 'empty', '.svg': 'empty' },
|
|
52
|
+
logLevel: 'silent',
|
|
53
|
+
});
|
|
54
|
+
const server = createServer((request, response) => {
|
|
55
|
+
if (request.url === '/easyrewrite.js' && process.env.DSH_EASYREWRITE_CLIENT) {
|
|
56
|
+
response.setHeader('Content-Type', 'text/javascript');
|
|
57
|
+
response.end(readFileSync(process.env.DSH_EASYREWRITE_CLIENT));
|
|
58
|
+
} else if (request.url === '/fixture.js') {
|
|
59
|
+
response.setHeader('Content-Type', 'text/javascript');
|
|
60
|
+
response.end(readFileSync(join(out, 'fixture.js')));
|
|
61
|
+
} else {
|
|
62
|
+
response.setHeader('Content-Type', 'text/html');
|
|
63
|
+
const easyrewrite = process.env.DSH_EASYREWRITE_CLIENT
|
|
64
|
+
? '<script>window.__ModuleLoader__={load:m=>window.easyrewriteModule=m}</script><script src="/easyrewrite.js"></script>' : '';
|
|
65
|
+
response.end(`<!doctype html><div id="app"></div>${easyrewrite}<script type="module" src="/fixture.js"></script>`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
69
|
+
let browser;
|
|
70
|
+
try {
|
|
71
|
+
browser = await launchPinnedChromium();
|
|
72
|
+
const page = await browser.newPage();
|
|
73
|
+
const errors: string[] = [];
|
|
74
|
+
page.on('pageerror', (error: Error) => errors.push(String(error)));
|
|
75
|
+
page.on('console', (message: { type(): string; text(): string }) => {
|
|
76
|
+
if (message.type() === 'error') errors.push(message.text());
|
|
77
|
+
});
|
|
78
|
+
const address = server.address();
|
|
79
|
+
assert.ok(address && typeof address === 'object');
|
|
80
|
+
await page.goto(`http://127.0.0.1:${address.port}`);
|
|
81
|
+
await page.waitForFunction(() => 'readerUserResult' in window, undefined, { timeout: 10000 });
|
|
82
|
+
assert.equal(await page.evaluate(() => Reflect.get(window, 'readerUserResult')), 'passed');
|
|
83
|
+
assert.deepEqual(errors, []);
|
|
84
|
+
} finally {
|
|
85
|
+
await browser?.close();
|
|
86
|
+
await new Promise<void>(resolve => server.close(() => resolve()));
|
|
87
|
+
}
|
|
88
|
+
} finally {
|
|
89
|
+
rmSync(out, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { homedir, tmpdir } from 'node:os';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import { inflateSync } from 'node:zlib';
|
|
10
|
+
import { test } from 'vitest';
|
|
11
|
+
|
|
12
|
+
const browserRuntime = join(homedir(), '.codex/playwright-runtime/runtime.mjs');
|
|
13
|
+
const skip = existsSync(browserRuntime) ? false : 'pinned Chromium runtime not available';
|
|
14
|
+
const root = resolve(import.meta.dirname, '..');
|
|
15
|
+
const shots = '/tmp/issue-66-sticky-lanes';
|
|
16
|
+
|
|
17
|
+
function paeth(left: number, up: number, upLeft: number) {
|
|
18
|
+
const estimate = left + up - upLeft;
|
|
19
|
+
const leftDistance = Math.abs(estimate - left);
|
|
20
|
+
const upDistance = Math.abs(estimate - up);
|
|
21
|
+
const diagonal = Math.abs(estimate - upLeft);
|
|
22
|
+
if (leftDistance <= upDistance && leftDistance <= diagonal) return left;
|
|
23
|
+
if (upDistance <= diagonal) return up;
|
|
24
|
+
return upLeft;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function pngPixel(buffer: Buffer, x: number, y: number) {
|
|
28
|
+
let offset = 8;
|
|
29
|
+
const chunks: Buffer[] = [];
|
|
30
|
+
let width = 0;
|
|
31
|
+
let height = 0;
|
|
32
|
+
let colorType = 0;
|
|
33
|
+
while (offset < buffer.length) {
|
|
34
|
+
const length = buffer.readUInt32BE(offset);
|
|
35
|
+
const type = buffer.toString('ascii', offset + 4, offset + 8);
|
|
36
|
+
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
|
37
|
+
if (type === 'IHDR') {
|
|
38
|
+
width = data.readUInt32BE(0);
|
|
39
|
+
height = data.readUInt32BE(4);
|
|
40
|
+
colorType = data[9] ?? 0;
|
|
41
|
+
} else if (type === 'IDAT') chunks.push(data);
|
|
42
|
+
else if (type === 'IEND') break;
|
|
43
|
+
offset += 12 + length;
|
|
44
|
+
}
|
|
45
|
+
const bytesPerPixel = colorType === 6 ? 4 : 3;
|
|
46
|
+
const raw = inflateSync(Buffer.concat(chunks));
|
|
47
|
+
const stride = width * bytesPerPixel;
|
|
48
|
+
let source = 0;
|
|
49
|
+
let previous = Buffer.alloc(stride);
|
|
50
|
+
const rows: Buffer[] = [];
|
|
51
|
+
for (let rowIndex = 0; rowIndex < height; rowIndex += 1) {
|
|
52
|
+
const filter = raw[source] ?? 0;
|
|
53
|
+
source += 1;
|
|
54
|
+
const row = Buffer.alloc(stride);
|
|
55
|
+
for (let index = 0; index < stride; index += 1) {
|
|
56
|
+
const value = raw[source] ?? 0;
|
|
57
|
+
source += 1;
|
|
58
|
+
const left = index >= bytesPerPixel ? row[index - bytesPerPixel] ?? 0 : 0;
|
|
59
|
+
const up = previous[index] ?? 0;
|
|
60
|
+
const upLeft = index >= bytesPerPixel ? previous[index - bytesPerPixel] ?? 0 : 0;
|
|
61
|
+
let decoded = value;
|
|
62
|
+
if (filter === 1) decoded = value + left;
|
|
63
|
+
else if (filter === 2) decoded = value + up;
|
|
64
|
+
else if (filter === 3) decoded = value + Math.floor((left + up) / 2);
|
|
65
|
+
else if (filter === 4) decoded = value + paeth(left, up, upLeft);
|
|
66
|
+
else if (filter !== 0) throw new Error(`unsupported png filter ${filter}`);
|
|
67
|
+
row[index] = decoded & 255;
|
|
68
|
+
}
|
|
69
|
+
rows.push(row);
|
|
70
|
+
previous = row;
|
|
71
|
+
}
|
|
72
|
+
const pixel = rows[y];
|
|
73
|
+
const start = x * bytesPerPixel;
|
|
74
|
+
return [pixel?.[start] ?? 0, pixel?.[start + 1] ?? 0, pixel?.[start + 2] ?? 0];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function dist(a: number[], b: number[]) {
|
|
78
|
+
return Math.hypot(a[0]! - b[0]!, a[1]! - b[1]!, a[2]! - b[2]!);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type Rgb = [number, number, number];
|
|
82
|
+
|
|
83
|
+
function laneHtml(cssName: string, options: {
|
|
84
|
+
scheme: 'dark' | 'light';
|
|
85
|
+
base: string;
|
|
86
|
+
layer: string;
|
|
87
|
+
label: string;
|
|
88
|
+
delta: number;
|
|
89
|
+
fontSize: number;
|
|
90
|
+
glass?: boolean;
|
|
91
|
+
}) {
|
|
92
|
+
const glass = options.glass ? ' data-reader-glass=""' : '';
|
|
93
|
+
return `<!doctype html>
|
|
94
|
+
<html lang="zh-CN">
|
|
95
|
+
<head>
|
|
96
|
+
<meta charset="utf-8">
|
|
97
|
+
<link rel="stylesheet" href="/${cssName}">
|
|
98
|
+
<style>
|
|
99
|
+
:root {
|
|
100
|
+
color-scheme: ${options.scheme};
|
|
101
|
+
--dsh-content-font-size: ${options.fontSize}px;
|
|
102
|
+
--dsh-content-font-delta: ${options.delta}px;
|
|
103
|
+
--dsw-alias-bg-base: ${options.base};
|
|
104
|
+
--dsw-alias-bg-layer-1: ${options.layer};
|
|
105
|
+
--dsw-alias-label-primary: ${options.label};
|
|
106
|
+
--dsw-alias-label-secondary: ${options.label};
|
|
107
|
+
--dsw-alias-label-tertiary: ${options.label};
|
|
108
|
+
--dsw-font-family: ui-sans-serif, sans-serif;
|
|
109
|
+
}
|
|
110
|
+
html, body { margin: 0; background: #ff00aa; color-scheme: ${options.scheme}; }
|
|
111
|
+
.scroll { height: 280px; overflow: auto; background: #ff00aa; }
|
|
112
|
+
#heading { margin: 0; background: #00e5ff; color: #ff4ad2; font: 700 48px/52px sans-serif; }
|
|
113
|
+
#canvas-probe { position: fixed; right: 8px; bottom: 8px; width: 12px; height: 12px; background: Canvas; }
|
|
114
|
+
</style>
|
|
115
|
+
</head>
|
|
116
|
+
<body>
|
|
117
|
+
<div id="canvas-probe"></div>
|
|
118
|
+
<div class="scroll" id="scroll">
|
|
119
|
+
<div class="root"${glass} data-dsh-better-display="fixture">
|
|
120
|
+
<div class="column" data-chat-flow="">
|
|
121
|
+
<div class="toolbar" data-reader-lane="toolbar"><button class="textButton" type="button">自动折叠开</button></div>
|
|
122
|
+
<section class="turn" data-reader-turn="1">
|
|
123
|
+
<div class="turnStickyRegion">
|
|
124
|
+
<div class="turnStickyLanes">
|
|
125
|
+
<div class="turnProcessSticky" data-reader-lane="status">
|
|
126
|
+
<button class="disclosureButton" type="button"><span class="statusText">用时 64 分 24 秒</span></button>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="closedProcessSummary" id="summary">
|
|
129
|
+
<div class="summaryRow">
|
|
130
|
+
<button class="disclosureButton" type="button"><span class="foldSummary">思考×79 · 输出×15</span></button>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="answer">
|
|
135
|
+
<h2 id="heading">核心对比核心对比核心对比核心对比</h2>
|
|
136
|
+
<div style="height:900px"></div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</section>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
<script>window.__ready = true;</script>
|
|
144
|
+
</body>
|
|
145
|
+
</html>`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function listen(files: Map<string, Buffer | string>) {
|
|
149
|
+
const server = createServer((request, response) => {
|
|
150
|
+
const body = files.get(request.url ?? '/');
|
|
151
|
+
if (body === undefined) { response.statusCode = 404; response.end(); return; }
|
|
152
|
+
response.setHeader('Content-Type', request.url?.endsWith('.css') ? 'text/css' : request.url?.endsWith('.js') ? 'text/javascript' : 'text/html');
|
|
153
|
+
response.end(body);
|
|
154
|
+
});
|
|
155
|
+
await new Promise<void>(resolveListen => server.listen(0, '127.0.0.1', resolveListen));
|
|
156
|
+
const address = server.address();
|
|
157
|
+
assert.ok(address && typeof address === 'object');
|
|
158
|
+
return {
|
|
159
|
+
port: address.port,
|
|
160
|
+
close: () => new Promise<void>(resolveClose => server.close(() => resolveClose())),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function sample(page: { screenshot(options: { clip: { x: number; y: number; width: number; height: number }; scale: 'css' }): Promise<Buffer> }, x: number, y: number): Promise<Rgb> {
|
|
165
|
+
const shot = await page.screenshot({ clip: { x: Math.round(x), y: Math.round(y), width: 1, height: 1 }, scale: 'css' });
|
|
166
|
+
const [red, green, blue] = pngPixel(shot, 0, 0);
|
|
167
|
+
return [red, green, blue];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
test('transparent and faded theme tokens keep lane pixels on the page surface', { skip, timeout: 30_000 }, async () => {
|
|
171
|
+
const css = readFileSync(join(root, 'src/client/Reader.module.css'));
|
|
172
|
+
const files = new Map<string, Buffer | string>([['/reader.css', css]]);
|
|
173
|
+
const cases = [
|
|
174
|
+
{ name: 'dark-22-clear', scheme: 'dark' as const, base: 'transparent', layer: 'transparent', label: '#f5f7fa', delta: 8, fontSize: 22, limit: 16 },
|
|
175
|
+
{ name: 'light-22-clear', scheme: 'light' as const, base: 'transparent', layer: 'transparent', label: '#141414', delta: 8, fontSize: 22, limit: 16 },
|
|
176
|
+
{ name: 'dark-22-fade', scheme: 'dark' as const, base: 'rgb(255 255 255 / 10%)', layer: 'rgb(255 255 255 / 10%)', label: '#f5f7fa', delta: 8, fontSize: 22, limit: 100 },
|
|
177
|
+
{ name: 'light-22-fade', scheme: 'light' as const, base: 'rgb(255 255 255 / 10%)', layer: 'rgb(255 255 255 / 10%)', label: '#141414', delta: 8, fontSize: 22, limit: 100 },
|
|
178
|
+
{ name: 'floor-12', scheme: 'dark' as const, base: 'transparent', layer: 'transparent', label: '#f5f7fa', delta: -2, fontSize: 12, limit: 16 },
|
|
179
|
+
];
|
|
180
|
+
for (const item of cases) files.set(`/${item.name}.html`, laneHtml('reader.css', item));
|
|
181
|
+
const server = await listen(files);
|
|
182
|
+
const { launchPinnedChromium } = await import(pathToFileURL(browserRuntime).href);
|
|
183
|
+
const browser = await launchPinnedChromium();
|
|
184
|
+
mkdirSync(shots, { recursive: true });
|
|
185
|
+
try {
|
|
186
|
+
const page = await browser.newPage({ viewport: { width: 1000, height: 360 }, deviceScaleFactor: 1 });
|
|
187
|
+
const errors: string[] = [];
|
|
188
|
+
page.on('pageerror', (error: Error) => errors.push(String(error)));
|
|
189
|
+
for (const item of cases) {
|
|
190
|
+
await page.goto(`http://127.0.0.1:${server.port}/${item.name}.html`);
|
|
191
|
+
await page.waitForFunction(() => Reflect.get(window, '__ready') === true);
|
|
192
|
+
const box = await page.evaluate(() => {
|
|
193
|
+
const rect = (selector: string) => {
|
|
194
|
+
const itemRect = document.querySelector(selector)!.getBoundingClientRect();
|
|
195
|
+
return { top: itemRect.top, right: itemRect.right, bottom: itemRect.bottom, left: itemRect.left, height: itemRect.height };
|
|
196
|
+
};
|
|
197
|
+
return { toolbar: rect('.toolbar'), status: rect('[data-reader-lane="status"]'), summary: rect('#summary'), probe: rect('#canvas-probe') };
|
|
198
|
+
});
|
|
199
|
+
if (item.fontSize === 12) {
|
|
200
|
+
assert.ok(box.toolbar.height >= 40, `${item.name} toolbar ${box.toolbar.height}px is under the 40px floor`);
|
|
201
|
+
assert.ok(box.status.height >= 40, `${item.name} status ${box.status.height}px is under the 40px floor`);
|
|
202
|
+
} else {
|
|
203
|
+
assert.ok(box.status.height > 44, `${item.name} status ${box.status.height}px did not grow with the 22px content font`);
|
|
204
|
+
}
|
|
205
|
+
await page.evaluate(() => {
|
|
206
|
+
const scroll = document.querySelector<HTMLElement>('#scroll')!;
|
|
207
|
+
const summary = document.querySelector('#summary')!.getBoundingClientRect();
|
|
208
|
+
const heading = document.querySelector('#heading')!.getBoundingClientRect();
|
|
209
|
+
scroll.scrollTop += heading.top - summary.top;
|
|
210
|
+
});
|
|
211
|
+
const points = await page.evaluate(() => {
|
|
212
|
+
const summary = document.querySelector('#summary')!.getBoundingClientRect();
|
|
213
|
+
const status = document.querySelector('[data-reader-lane="status"]')!.getBoundingClientRect();
|
|
214
|
+
const probe = document.querySelector('#canvas-probe')!.getBoundingClientRect();
|
|
215
|
+
return {
|
|
216
|
+
summary: { x: summary.left + 10, y: summary.top + 2 },
|
|
217
|
+
status: { x: status.right - 12, y: status.top + 2 },
|
|
218
|
+
probe: { x: probe.left + 2, y: probe.top + 2 },
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
const canvas = await sample(page, points.probe.x, points.probe.y);
|
|
222
|
+
const summary = await sample(page, points.summary.x, points.summary.y);
|
|
223
|
+
const status = await sample(page, points.status.x, points.status.y);
|
|
224
|
+
const text = item.scheme === 'dark' ? [245, 247, 250] : [20, 20, 20];
|
|
225
|
+
const glyph: Rgb = [255, 74, 210];
|
|
226
|
+
const wash: Rgb = [0, 229, 255];
|
|
227
|
+
const pageTone: Rgb = [255, 0, 170];
|
|
228
|
+
await page.screenshot({ path: join(shots, `${item.name}.png`) });
|
|
229
|
+
for (const [place, pixel] of [['summary', summary], ['status', status]] as const) {
|
|
230
|
+
const message = `${item.name} ${place} pixel ${pixel.join(',')} canvas ${canvas.join(',')} text ${text.join(',')}`;
|
|
231
|
+
assert.ok(dist(pixel, canvas) <= item.limit, message);
|
|
232
|
+
assert.ok(dist(pixel, canvas) + 40 < dist(pixel, text), message);
|
|
233
|
+
assert.ok(dist(pixel, glyph) > 80, message);
|
|
234
|
+
assert.ok(dist(pixel, wash) > 80, message);
|
|
235
|
+
assert.ok(dist(pixel, pageTone) > 80, message);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
assert.deepEqual(errors, []);
|
|
239
|
+
} finally {
|
|
240
|
+
await browser.close();
|
|
241
|
+
await server.close();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('stock and glass lanes stay pixel-identical to origin/main', { skip, timeout: 30_000 }, async () => {
|
|
246
|
+
const headCss = readFileSync(join(root, 'src/client/Reader.module.css'));
|
|
247
|
+
const mainCss = execFileSync('git', ['show', 'origin/main:src/client/Reader.module.css'], { cwd: root });
|
|
248
|
+
const files = new Map<string, Buffer | string>([
|
|
249
|
+
['/head.css', headCss],
|
|
250
|
+
['/main.css', mainCss],
|
|
251
|
+
['/stock-head.html', laneHtml('head.css', { scheme: 'light', base: '#223344', layer: '#aabbcc', label: '#f5f7fa', delta: 0, fontSize: 14 })],
|
|
252
|
+
['/stock-main.html', laneHtml('main.css', { scheme: 'light', base: '#223344', layer: '#aabbcc', label: '#f5f7fa', delta: 0, fontSize: 14 })],
|
|
253
|
+
['/glass-head.html', laneHtml('head.css', { scheme: 'light', base: '#223344', layer: '#aabbcc', label: '#f5f7fa', delta: 0, fontSize: 14, glass: true })],
|
|
254
|
+
['/glass-main.html', laneHtml('main.css', { scheme: 'light', base: '#223344', layer: '#aabbcc', label: '#f5f7fa', delta: 0, fontSize: 14, glass: true })],
|
|
255
|
+
]);
|
|
256
|
+
const server = await listen(files);
|
|
257
|
+
const { launchPinnedChromium } = await import(pathToFileURL(browserRuntime).href);
|
|
258
|
+
const browser = await launchPinnedChromium();
|
|
259
|
+
mkdirSync(shots, { recursive: true });
|
|
260
|
+
try {
|
|
261
|
+
const page = await browser.newPage({ viewport: { width: 1000, height: 360 }, deviceScaleFactor: 1 });
|
|
262
|
+
const read = async (path: string) => {
|
|
263
|
+
await page.goto(`http://127.0.0.1:${server.port}${path}`);
|
|
264
|
+
await page.waitForFunction(() => Reflect.get(window, '__ready') === true);
|
|
265
|
+
const points = await page.evaluate(() => {
|
|
266
|
+
const summary = document.querySelector('#summary')!.getBoundingClientRect();
|
|
267
|
+
const toolbar = document.querySelector('.toolbar')!.getBoundingClientRect();
|
|
268
|
+
return {
|
|
269
|
+
lane: { x: summary.left + 12, y: summary.top + 2 },
|
|
270
|
+
gap: { x: summary.left + 12, y: summary.top - 4 },
|
|
271
|
+
toolbar: { x: toolbar.left + 8, y: toolbar.top + 4 },
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
return {
|
|
275
|
+
lane: await sample(page, points.lane.x, points.lane.y),
|
|
276
|
+
gap: await sample(page, points.gap.x, points.gap.y),
|
|
277
|
+
toolbar: await sample(page, points.toolbar.x, points.toolbar.y),
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
const stockHead = await read('/stock-head.html');
|
|
281
|
+
await page.screenshot({ path: join(shots, 'stock-head.png') });
|
|
282
|
+
const stockMain = await read('/stock-main.html');
|
|
283
|
+
await page.screenshot({ path: join(shots, 'stock-main.png') });
|
|
284
|
+
const glassHead = await read('/glass-head.html');
|
|
285
|
+
await page.screenshot({ path: join(shots, 'glass-head.png') });
|
|
286
|
+
const glassMain = await read('/glass-main.html');
|
|
287
|
+
await page.screenshot({ path: join(shots, 'glass-main.png') });
|
|
288
|
+
for (const place of ['lane', 'gap', 'toolbar'] as const) {
|
|
289
|
+
assert.ok(dist(stockHead[place], stockMain[place]) <= 2, `stock ${place} head ${stockHead[place].join(',')} main ${stockMain[place].join(',')}`);
|
|
290
|
+
assert.ok(dist(stockHead[place], [34, 51, 68]) <= 2, `stock ${place} left the opaque token: ${stockHead[place].join(',')}`);
|
|
291
|
+
assert.ok(dist(glassHead[place], glassMain[place]) <= 8, `glass ${place} head ${glassHead[place].join(',')} main ${glassMain[place].join(',')}`);
|
|
292
|
+
assert.ok(dist(glassHead[place], [255, 0, 170]) <= 12, `glass ${place} painted a plate: ${glassHead[place].join(',')}`);
|
|
293
|
+
}
|
|
294
|
+
} finally {
|
|
295
|
+
await browser.close();
|
|
296
|
+
await server.close();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('each Reader root owns its lane height and the floor is 40px', { skip, timeout: 30_000 }, async () => {
|
|
301
|
+
const harnessPointer = join(homedir(), '.config/dshx/harness');
|
|
302
|
+
const harness = process.env.DSHX_HARNESS?.trim()
|
|
303
|
+
|| (existsSync(harnessPointer) ? readFileSync(harnessPointer, 'utf8').trim() : '');
|
|
304
|
+
assert.ok(harness, 'DSH Harness source not available');
|
|
305
|
+
const require = createRequire(join(root, 'package.json'));
|
|
306
|
+
const { build } = createRequire(require.resolve('tsx'))('esbuild');
|
|
307
|
+
const webRequire = createRequire(join(harness, 'packages/client/web/package.json'));
|
|
308
|
+
const out = mkdtempSync(join(tmpdir(), 'reader-lane-owners-'));
|
|
309
|
+
try {
|
|
310
|
+
await build({
|
|
311
|
+
entryPoints: [join(root, 'tests/fixtures/reader-lane-owners.tsx')],
|
|
312
|
+
outfile: join(out, 'fixture.js'),
|
|
313
|
+
bundle: true,
|
|
314
|
+
platform: 'browser',
|
|
315
|
+
format: 'esm',
|
|
316
|
+
jsx: 'automatic',
|
|
317
|
+
alias: {
|
|
318
|
+
react: dirnameOf(require.resolve('react/package.json')),
|
|
319
|
+
'react-dom': dirnameOf(webRequire.resolve('react-dom/package.json')),
|
|
320
|
+
},
|
|
321
|
+
logLevel: 'silent',
|
|
322
|
+
});
|
|
323
|
+
const server = await listen(new Map<string, Buffer | string>([
|
|
324
|
+
['/fixture.js', readFileSync(join(out, 'fixture.js'))],
|
|
325
|
+
['/', '<!doctype html><div id="app"></div><script type="module" src="/fixture.js"></script>'],
|
|
326
|
+
]));
|
|
327
|
+
const { launchPinnedChromium } = await import(pathToFileURL(browserRuntime).href);
|
|
328
|
+
const browser = await launchPinnedChromium();
|
|
329
|
+
try {
|
|
330
|
+
const page = await browser.newPage({ viewport: { width: 800, height: 600 }, deviceScaleFactor: 1 });
|
|
331
|
+
const errors: string[] = [];
|
|
332
|
+
page.on('pageerror', (error: Error) => errors.push(String(error)));
|
|
333
|
+
await page.goto(`http://127.0.0.1:${server.port}/`);
|
|
334
|
+
await page.waitForSelector('#row-a');
|
|
335
|
+
await page.click('#grow');
|
|
336
|
+
await page.waitForFunction(() => {
|
|
337
|
+
const row = document.querySelector('#row-a');
|
|
338
|
+
return !!row && row.getBoundingClientRect().height >= 86;
|
|
339
|
+
});
|
|
340
|
+
await page.evaluate(() => new Promise(resolveFrame => requestAnimationFrame(() => requestAnimationFrame(resolveFrame))));
|
|
341
|
+
const grown = await page.evaluate(() => ({
|
|
342
|
+
published: document.querySelector<HTMLElement>('#turn-a')!.style.getPropertyValue('--reader-summary-height'),
|
|
343
|
+
actual: document.querySelector('#row-a')!.getBoundingClientRect().height,
|
|
344
|
+
other: document.querySelector<HTMLElement>('#turn-b')!.style.getPropertyValue('--reader-summary-height'),
|
|
345
|
+
short: document.querySelector<HTMLElement>('#turn-short')!.style.getPropertyValue('--reader-status-height'),
|
|
346
|
+
toolbar: Number.parseFloat(document.querySelector<HTMLElement>('#root-toolbar')!.style.getPropertyValue('--reader-toolbar-height')),
|
|
347
|
+
status: Number.parseFloat(document.querySelector<HTMLElement>('#turn-status')!.style.getPropertyValue('--reader-status-height')),
|
|
348
|
+
}));
|
|
349
|
+
assert.equal(grown.actual, 86, `row a measured ${grown.actual}`);
|
|
350
|
+
assert.equal(grown.published, '86px', `reader A kept a stale lane height ${grown.published} for an 86px row`);
|
|
351
|
+
assert.equal(grown.other, '52px', `reader B lane height changed to ${grown.other}`);
|
|
352
|
+
assert.equal(grown.short, '40px', `38px row published ${grown.short}`);
|
|
353
|
+
assert.ok(grown.toolbar >= 40, `toolbar lane published ${grown.toolbar}`);
|
|
354
|
+
assert.ok(grown.status >= 40, `status lane published ${grown.status}`);
|
|
355
|
+
await page.click('#drop');
|
|
356
|
+
await page.waitForFunction(() => document.querySelector('#row-a') === null);
|
|
357
|
+
await page.evaluate(() => new Promise(resolveFrame => requestAnimationFrame(() => requestAnimationFrame(resolveFrame))));
|
|
358
|
+
const dropped = await page.evaluate(() => ({
|
|
359
|
+
published: document.querySelector<HTMLElement>('#turn-a')!.style.getPropertyValue('--reader-summary-height'),
|
|
360
|
+
other: document.querySelector<HTMLElement>('#turn-b')!.style.getPropertyValue('--reader-summary-height'),
|
|
361
|
+
}));
|
|
362
|
+
assert.equal(dropped.published, '', `unmounted reader A left ${dropped.published}`);
|
|
363
|
+
assert.equal(dropped.other, '52px', `unmounting reader A cleared reader B (${dropped.other})`);
|
|
364
|
+
assert.deepEqual(errors, []);
|
|
365
|
+
await page.screenshot({ path: join(shots, 'two-roots.png') });
|
|
366
|
+
} finally {
|
|
367
|
+
await browser.close();
|
|
368
|
+
await server.close();
|
|
369
|
+
}
|
|
370
|
+
} finally {
|
|
371
|
+
rmSync(out, { recursive: true, force: true });
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
function dirnameOf(file: string) {
|
|
376
|
+
return file.slice(0, file.lastIndexOf('/'));
|
|
377
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { test } from 'vitest';
|
|
5
|
+
import { externalClientBundle } from '../tools/client-build.js';
|
|
6
|
+
|
|
7
|
+
const css = readFileSync(resolve('src/client/Reader.module.css'), 'utf8');
|
|
8
|
+
|
|
9
|
+
test('sticky lanes follow the host font delta and measured row heights', () => {
|
|
10
|
+
assert.match(css, /--reader-lane-block:\s*max\(40px,\s*calc\(40px \+ var\(--dsh-content-font-delta, 0px\)\)\)/);
|
|
11
|
+
assert.match(css, /font-size:\s*calc\(14px \+ var\(--dsh-content-font-delta, 0px\)\)/);
|
|
12
|
+
assert.match(css, /line-height:\s*calc\(24px \+ var\(--dsh-content-font-delta, 0px\)\)/);
|
|
13
|
+
assert.match(
|
|
14
|
+
css,
|
|
15
|
+
/\.flowCell\[data-flow-summary\], \.closedProcessSummary \{[^}]*top:\s*calc\(var\(--reader-toolbar-height, var\(--reader-lane-block, 40px\)\) \+ var\(--reader-status-height, var\(--reader-lane-block, 40px\)\)\)/,
|
|
16
|
+
);
|
|
17
|
+
assert.match(css, /--reader-code-summary-height:\s*var\(--reader-summary-height, var\(--reader-lane-block, 40px\)\)/);
|
|
18
|
+
assert.doesNotMatch(css, /--reader-sticky-lane/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('a transparent canvas still masks the lane with the surface token', () => {
|
|
22
|
+
assert.match(css, /--reader-lane-plate:\s*linear-gradient\(var\(--dsw-alias-bg-base\)/);
|
|
23
|
+
assert.match(css, /linear-gradient\(var\(--dsw-alias-bg-layer-1, transparent\)/);
|
|
24
|
+
assert.doesNotMatch(css, /--reader-lane-plate:[^;]*label-primary/);
|
|
25
|
+
assert.doesNotMatch(css, /--reader-lane-plate:[^;]*color-mix/);
|
|
26
|
+
assert.match(css, /background-color:\s*Canvas/);
|
|
27
|
+
assert.match(css, /background-image:\s*var\(--reader-lane-plate\)/);
|
|
28
|
+
assert.match(css, /\.turnProcessSticky \{ box-shadow:[^;]*\bCanvas\b/);
|
|
29
|
+
assert.doesNotMatch(css, /\.turnProcessSticky \{ box-shadow:[^;]*label-primary/);
|
|
30
|
+
assert.match(css, /\.closedProcessSummary \{ box-shadow:[^;]*\bCanvas\b/);
|
|
31
|
+
assert.match(css, /\.flowCell\[data-flow-summary\] \{ box-shadow:[^;]*\bCanvas\b/);
|
|
32
|
+
assert.match(css, /\.root\[data-reader-glass\] \.turnProcessSticky/);
|
|
33
|
+
const glass = css.slice(css.indexOf('Skin mode is opt-in'));
|
|
34
|
+
assert.match(glass, /background:\s*transparent/);
|
|
35
|
+
assert.doesNotMatch(glass, /background-color:\s*Canvas/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('built Reader CSS keeps the lane plate and the measured summary offset', async () => {
|
|
39
|
+
const configs = externalClientBundle('dsh-better-display', [], { clientEntry: 'src/client/index.tsx' });
|
|
40
|
+
const client = configs.find(config => config.name === 'dsh-better-display/client');
|
|
41
|
+
const plugin = client?.plugins.find(entry => entry.name === 'dshx-css-modules-inline');
|
|
42
|
+
assert.ok(plugin);
|
|
43
|
+
const source = await plugin.load.call({}, `\0dshx-css-module:${resolve('src/client/Reader.module.css')}.mjs`);
|
|
44
|
+
const literal = /^const css = (.+);$/m.exec(source)?.[1];
|
|
45
|
+
assert.ok(literal);
|
|
46
|
+
const built: unknown = JSON.parse(literal);
|
|
47
|
+
assert.equal(typeof built, 'string');
|
|
48
|
+
if (typeof built !== 'string') return;
|
|
49
|
+
assert.match(built, /--reader-lane-block:max\(40px, calc\(40px \+ var\(--dsh-content-font-delta,0px\)\)\)/);
|
|
50
|
+
assert.match(built, /--dsw-alias-bg-layer-1,transparent/);
|
|
51
|
+
assert.match(built, /background-color:canvas/);
|
|
52
|
+
assert.doesNotMatch(built, /--reader-lane-plate:[^;]*label-primary/);
|
|
53
|
+
assert.match(built, /var\(--reader-toolbar-height,var\(--reader-lane-block,40px\)\) \+ var\(--reader-status-height,var\(--reader-lane-block,40px\)\)/);
|
|
54
|
+
});
|