@etamong-playground/ui 0.34.2

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.
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ var msw = require('msw');
4
+ var test = require('@playwright/test');
5
+
6
+ // src/testing-viewport-fit.ts
7
+ var VIEWPORT_FIT_WARN_PREFIX = "[@etamong-playground/ui]";
8
+ var FLEET_VIEWPORT_PROFILES = [
9
+ {
10
+ name: "iphone-17-pro-pwa",
11
+ viewport: { width: 402, height: 874 },
12
+ standalone: true,
13
+ safeAreaTop: 59,
14
+ safeAreaBottom: 34
15
+ },
16
+ {
17
+ name: "ipad-mini-portrait",
18
+ viewport: { width: 768, height: 1024 }
19
+ },
20
+ {
21
+ name: "desktop-1280",
22
+ viewport: { width: 1280, height: 800 }
23
+ },
24
+ {
25
+ name: "android-narrow",
26
+ viewport: { width: 360, height: 780 }
27
+ }
28
+ ];
29
+ async function assertViewportFit(page, urls, options = {}) {
30
+ const profiles = options.profiles && options.profiles.length ? options.profiles : FLEET_VIEWPORT_PROFILES;
31
+ const settleMs = options.settleMs ?? 400;
32
+ const predicate = options.predicate ?? ((msg) => msg.type() === "warning" && msg.text().startsWith(VIEWPORT_FIT_WARN_PREFIX));
33
+ const collected = [];
34
+ const onConsole = (msg) => {
35
+ if (predicate(msg)) collected.push(msg.text());
36
+ };
37
+ page.on("console", onConsole);
38
+ try {
39
+ for (const profile of profiles) {
40
+ await page.setViewportSize(profile.viewport);
41
+ if (profile.standalone) {
42
+ await page.emulateMedia({ media: "screen", colorScheme: "no-preference" });
43
+ await page.addInitScript(() => {
44
+ const origMatchMedia = window.matchMedia;
45
+ window.matchMedia = (q) => {
46
+ if (q.includes("display-mode: standalone")) {
47
+ return {
48
+ matches: true,
49
+ media: q,
50
+ onchange: null,
51
+ addListener: () => {
52
+ },
53
+ removeListener: () => {
54
+ },
55
+ addEventListener: () => {
56
+ },
57
+ removeEventListener: () => {
58
+ },
59
+ dispatchEvent: () => true
60
+ };
61
+ }
62
+ return origMatchMedia.call(window, q);
63
+ };
64
+ Object.defineProperty(navigator, "standalone", {
65
+ configurable: true,
66
+ value: true
67
+ });
68
+ });
69
+ }
70
+ if (profile.safeAreaTop != null || profile.safeAreaBottom != null) {
71
+ const top = profile.safeAreaTop ?? 0;
72
+ const bot = profile.safeAreaBottom ?? 0;
73
+ await page.addInitScript(
74
+ (insets) => {
75
+ const style = document.createElement("style");
76
+ style.textContent = `:root {
77
+ --etu-test-safe-top: ${insets.top}px;
78
+ --etu-test-safe-bot: ${insets.bot}px;
79
+ }
80
+ /* Stub env() so the browser sees a real inset value during
81
+ headless testing. CSS @supports cannot polyfill env(), so
82
+ apps that want this assertion to run under emulated
83
+ notched insets must use the .etu-test-safe-top/bot custom
84
+ properties instead of env() in tests \u2014 or apps can keep
85
+ env() and accept that desktop Chromium reports 0. */
86
+ `;
87
+ document.head.appendChild(style);
88
+ },
89
+ { top, bot }
90
+ );
91
+ }
92
+ for (const url of urls) {
93
+ collected.length = 0;
94
+ await page.goto(url, { waitUntil: "networkidle" });
95
+ await page.waitForTimeout(settleMs);
96
+ if (collected.length > 0) {
97
+ throw new Error(
98
+ `viewport-fit assertion failed at ${profile.name} on ${url}:
99
+ ` + collected.map((l) => " - " + l).join("\n")
100
+ );
101
+ }
102
+ }
103
+ }
104
+ } finally {
105
+ page.off("console", onConsole);
106
+ }
107
+ }
108
+ function defineViewportFitTests(test, options) {
109
+ const profiles = options.profiles && options.profiles.length ? options.profiles : FLEET_VIEWPORT_PROFILES;
110
+ const prefix = options.titlePrefix ?? "viewport-fit";
111
+ for (const profile of profiles) {
112
+ test(`${prefix} \u2014 ${profile.name}`, async (args) => {
113
+ if (options.beforeEach) await options.beforeEach(args);
114
+ await assertViewportFit(args.page, options.urls, {
115
+ profiles: [profile]
116
+ });
117
+ });
118
+ }
119
+ }
120
+ function mockHttperrRef() {
121
+ const b = new Uint8Array(4);
122
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
123
+ crypto.getRandomValues(b);
124
+ } else {
125
+ for (let i = 0; i < 4; i++) b[i] = Math.floor(Math.random() * 256);
126
+ }
127
+ return Array.from(b).map((x) => x.toString(16).padStart(2, "0")).join("");
128
+ }
129
+ function httperrBody(message, ref) {
130
+ return { error: message, ref: ref ?? mockHttperrRef() };
131
+ }
132
+ function createMeHandler(me = {}, path = "/api/v1/me") {
133
+ const body = {
134
+ email: me.email ?? "you@etamong.com",
135
+ is_admin: me.is_admin ?? false,
136
+ via_token: me.via_token ?? false,
137
+ can_create_apps: me.can_create_apps ?? false
138
+ };
139
+ return msw.http.get(path, () => msw.HttpResponse.json(body));
140
+ }
141
+ function createMeSignedOutHandler(path = "/api/v1/me") {
142
+ return msw.http.get(
143
+ path,
144
+ () => msw.HttpResponse.json(httperrBody("not signed in"), { status: 401 })
145
+ );
146
+ }
147
+ function createHealthzHandler(path = "/healthz") {
148
+ return msw.http.get(path, () => new msw.HttpResponse("ok", { status: 200 }));
149
+ }
150
+ function defaultMockHandlers(me = {}, paths = {}) {
151
+ return [createMeHandler(me, paths.me), createHealthzHandler(paths.healthz)];
152
+ }
153
+ var fleetTest = test.test.extend({
154
+ // eslint-disable-next-line no-empty-pattern
155
+ fleetPage: async ({ page }, use) => {
156
+ await use(page);
157
+ },
158
+ page: async ({ browser }, use) => {
159
+ const context = await browser.newContext({
160
+ locale: "ko-KR",
161
+ timezoneId: "Asia/Seoul",
162
+ viewport: { width: 1280, height: 800 },
163
+ extraHTTPHeaders: { "Accept-Language": "ko-KR" }
164
+ });
165
+ const page = await context.newPage();
166
+ try {
167
+ await use(page);
168
+ } finally {
169
+ await page.close();
170
+ await context.close();
171
+ }
172
+ }
173
+ });
174
+
175
+ exports.FLEET_VIEWPORT_PROFILES = FLEET_VIEWPORT_PROFILES;
176
+ exports.VIEWPORT_FIT_WARN_PREFIX = VIEWPORT_FIT_WARN_PREFIX;
177
+ exports.assertViewportFit = assertViewportFit;
178
+ exports.createHealthzHandler = createHealthzHandler;
179
+ exports.createMeHandler = createMeHandler;
180
+ exports.createMeSignedOutHandler = createMeSignedOutHandler;
181
+ exports.defaultMockHandlers = defaultMockHandlers;
182
+ exports.defineViewportFitTests = defineViewportFitTests;
183
+ exports.fleetTest = fleetTest;
184
+ exports.httperrBody = httperrBody;
185
+ exports.mockHttperrRef = mockHttperrRef;
@@ -0,0 +1,166 @@
1
+ import * as _playwright_test from '@playwright/test';
2
+ import { ConsoleMessage, PlaywrightTestArgs, Page } from '@playwright/test';
3
+ export { Page, PlaywrightTestArgs, PlaywrightTestConfig } from '@playwright/test';
4
+ import { HttpHandler } from 'msw';
5
+
6
+ /**
7
+ * Playwright helpers for the fleet viewport-fit assertions.
8
+ *
9
+ * The chrome primitives (`<NavigationBar>` v0.28.1+, MobileTabBar/Sidebar
10
+ * to follow) emit dev-mode `console.warn` lines prefixed with
11
+ * `[@etamong-playground/ui]` when layout breaks (off-screen bar, shrunk title,
12
+ * stacked safe-area-top). This module lifts the dev-mode signal into a
13
+ * CI gate.
14
+ *
15
+ * 1. Apps add a Playwright spec that calls `assertViewportFit(page,
16
+ * urls)`.
17
+ * 2. CI runs it at each fleet device profile (mobile/tablet/desktop +
18
+ * iPhone-Pro-PWA).
19
+ * 3. Any `[@etamong-playground/ui]` warning fails the test with the original
20
+ * message — so the diagnosis is the same one the developer would
21
+ * have seen at localhost.
22
+ *
23
+ * Peer dependency: `@playwright/test` (peer, not bundled). Apps that
24
+ * don't run e2e simply don't import this entry; nothing is pulled.
25
+ *
26
+ * See planning wiki: concepts/viewport-fit-assertions.md.
27
+ */
28
+
29
+ /** Marker the chrome primitives prefix every layout warning with. */
30
+ declare const VIEWPORT_FIT_WARN_PREFIX = "[@etamong-playground/ui]";
31
+ /**
32
+ * Canonical device profiles to drive viewport-fit checks at. These
33
+ * cover the four corners the regression has historically slipped past:
34
+ * iPhone Pro PWA (notch, narrow), iPad Mini (tablet rail boundary),
35
+ * desktop (sidebar), small Android (mobile tab bar).
36
+ *
37
+ * Names match Playwright's `devices` map where possible so apps can
38
+ * also reuse the profile directly.
39
+ */
40
+ interface DeviceProfile {
41
+ name: string;
42
+ viewport: {
43
+ width: number;
44
+ height: number;
45
+ };
46
+ /** When true, set the page to standalone-display-mode for iOS PWA. */
47
+ standalone?: boolean;
48
+ /** Emulate notched device safe-area-insets via a meta tag patch. */
49
+ safeAreaTop?: number;
50
+ safeAreaBottom?: number;
51
+ }
52
+ declare const FLEET_VIEWPORT_PROFILES: DeviceProfile[];
53
+ interface AssertViewportFitOptions {
54
+ /**
55
+ * Profiles to iterate. Default: `FLEET_VIEWPORT_PROFILES`. Pass an
56
+ * empty array to run only the currently-set page viewport.
57
+ */
58
+ profiles?: DeviceProfile[];
59
+ /**
60
+ * Extra time (ms) to settle after navigation before reading the
61
+ * captured warnings. Default 400 — gives the primitive's two
62
+ * `requestAnimationFrame` ticks plus a buffer for `env()` to apply.
63
+ */
64
+ settleMs?: number;
65
+ /**
66
+ * Additional console-message predicate. Default matches anything
67
+ * starting with `[@etamong-playground/ui]`.
68
+ */
69
+ predicate?: (msg: ConsoleMessage) => boolean;
70
+ }
71
+ /**
72
+ * Open each URL at each profile and assert no chrome primitive emitted
73
+ * a `[@etamong-playground/ui]` warning. The first violation throws with the
74
+ * original message — the failing test's stack carries the URL + profile.
75
+ */
76
+ declare function assertViewportFit(page: Page, urls: string[], options?: AssertViewportFitOptions): Promise<void>;
77
+ /**
78
+ * Convenience: a Playwright test that wraps `assertViewportFit` and
79
+ * fans out across profiles as separate test cases (so the failure
80
+ * report names the profile). Apps wire this in their `tests/` dir as:
81
+ *
82
+ * ```ts
83
+ * import { defineViewportFitTests } from "@etamong-playground/ui/testing";
84
+ * defineViewportFitTests({
85
+ * urls: ["/", "/settings", "/me"],
86
+ * beforeEach: async ({ page }) => { /* auth, mocks */ interface DefineViewportFitTestsOptions {
87
+ urls: string[];
88
+ profiles?: DeviceProfile[];
89
+ beforeEach?: (args: PlaywrightTestArgs) => Promise<void> | void;
90
+ /** Optional title prefix. Default: "viewport-fit". */
91
+ titlePrefix?: string;
92
+ }
93
+ declare function defineViewportFitTests(test: any, options: DefineViewportFitTestsOptions): void;
94
+
95
+ /**
96
+ * Cross-app MSW helpers. Optional — apps that use a different mock layer
97
+ * don't pull `msw` in (it's a peer dep).
98
+ *
99
+ * Two contracts every etamong-lab webui shares:
100
+ *
101
+ * 1. `/me` from oauth2-proxy/Authentik returns `{email, is_admin, ...}`.
102
+ * 2. Errors carry `{error, ref}` per `@etamong-lab/httperr`. Tests want a
103
+ * one-liner to produce a realistic failure.
104
+ *
105
+ * Both live here so a per-app `handlers.ts` only encodes the app's own
106
+ * surface, not these standard shapes.
107
+ */
108
+
109
+ /** Random 8-hex ref, matching the httperr server-side format. */
110
+ declare function mockHttperrRef(): string;
111
+ /**
112
+ * httperr-shaped error body. Pass to `HttpResponse.json` with the desired
113
+ * status so MSW returns the same shape as the real apiserver.
114
+ */
115
+ declare function httperrBody(message: string, ref?: string): {
116
+ error: string;
117
+ ref: string;
118
+ };
119
+ interface MockMe {
120
+ email?: string;
121
+ is_admin?: boolean;
122
+ via_token?: boolean;
123
+ can_create_apps?: boolean;
124
+ }
125
+ /**
126
+ * `/me` handler at the standard fleet path. Pass any subset of fields; the
127
+ * rest fall back to sensible test defaults.
128
+ */
129
+ declare function createMeHandler(me?: MockMe, path?: string): HttpHandler;
130
+ /**
131
+ * `/me` that returns the oauth2-proxy "not signed in" 401, so the webui's
132
+ * sign-in flow renders.
133
+ */
134
+ declare function createMeSignedOutHandler(path?: string): HttpHandler;
135
+ /**
136
+ * Default healthz — every fleet apiserver exposes one and tests often hit it
137
+ * in setup teardown.
138
+ */
139
+ declare function createHealthzHandler(path?: string): HttpHandler;
140
+ /**
141
+ * Convenience bundle: `/me` (signed-in by default) + `/healthz`. The first
142
+ * call site usually wants both; pass overrides as needed.
143
+ */
144
+ declare function defaultMockHandlers(me?: MockMe, paths?: {
145
+ me?: string;
146
+ healthz?: string;
147
+ }): HttpHandler[];
148
+
149
+ interface FleetFixtures {
150
+ /** Hook called once per test; passed the page after locale/viewport setup. */
151
+ fleetPage: Page;
152
+ }
153
+ /**
154
+ * Extended Playwright `test` that:
155
+ * - sets `Accept-Language: ko-KR` and locale ko-KR (matches the production
156
+ * i18n default per planning concept fleet-language-policy)
157
+ * - sets timezone to Asia/Seoul so date assertions are stable
158
+ * - sets a 1280×800 desktop viewport unless the test overrides it
159
+ *
160
+ * Use:
161
+ * import { fleetTest as test } from "@etamong-playground/ui/testing";
162
+ * test("works", async ({ page }) => { ... });
163
+ */
164
+ declare const fleetTest: _playwright_test.TestType<PlaywrightTestArgs & _playwright_test.PlaywrightTestOptions & FleetFixtures, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
165
+
166
+ export { type AssertViewportFitOptions, type DefineViewportFitTestsOptions, type DeviceProfile, FLEET_VIEWPORT_PROFILES, type FleetFixtures, type MockMe, VIEWPORT_FIT_WARN_PREFIX, assertViewportFit, createHealthzHandler, createMeHandler, createMeSignedOutHandler, defaultMockHandlers, defineViewportFitTests, fleetTest, httperrBody, mockHttperrRef };
@@ -0,0 +1,166 @@
1
+ import * as _playwright_test from '@playwright/test';
2
+ import { ConsoleMessage, PlaywrightTestArgs, Page } from '@playwright/test';
3
+ export { Page, PlaywrightTestArgs, PlaywrightTestConfig } from '@playwright/test';
4
+ import { HttpHandler } from 'msw';
5
+
6
+ /**
7
+ * Playwright helpers for the fleet viewport-fit assertions.
8
+ *
9
+ * The chrome primitives (`<NavigationBar>` v0.28.1+, MobileTabBar/Sidebar
10
+ * to follow) emit dev-mode `console.warn` lines prefixed with
11
+ * `[@etamong-playground/ui]` when layout breaks (off-screen bar, shrunk title,
12
+ * stacked safe-area-top). This module lifts the dev-mode signal into a
13
+ * CI gate.
14
+ *
15
+ * 1. Apps add a Playwright spec that calls `assertViewportFit(page,
16
+ * urls)`.
17
+ * 2. CI runs it at each fleet device profile (mobile/tablet/desktop +
18
+ * iPhone-Pro-PWA).
19
+ * 3. Any `[@etamong-playground/ui]` warning fails the test with the original
20
+ * message — so the diagnosis is the same one the developer would
21
+ * have seen at localhost.
22
+ *
23
+ * Peer dependency: `@playwright/test` (peer, not bundled). Apps that
24
+ * don't run e2e simply don't import this entry; nothing is pulled.
25
+ *
26
+ * See planning wiki: concepts/viewport-fit-assertions.md.
27
+ */
28
+
29
+ /** Marker the chrome primitives prefix every layout warning with. */
30
+ declare const VIEWPORT_FIT_WARN_PREFIX = "[@etamong-playground/ui]";
31
+ /**
32
+ * Canonical device profiles to drive viewport-fit checks at. These
33
+ * cover the four corners the regression has historically slipped past:
34
+ * iPhone Pro PWA (notch, narrow), iPad Mini (tablet rail boundary),
35
+ * desktop (sidebar), small Android (mobile tab bar).
36
+ *
37
+ * Names match Playwright's `devices` map where possible so apps can
38
+ * also reuse the profile directly.
39
+ */
40
+ interface DeviceProfile {
41
+ name: string;
42
+ viewport: {
43
+ width: number;
44
+ height: number;
45
+ };
46
+ /** When true, set the page to standalone-display-mode for iOS PWA. */
47
+ standalone?: boolean;
48
+ /** Emulate notched device safe-area-insets via a meta tag patch. */
49
+ safeAreaTop?: number;
50
+ safeAreaBottom?: number;
51
+ }
52
+ declare const FLEET_VIEWPORT_PROFILES: DeviceProfile[];
53
+ interface AssertViewportFitOptions {
54
+ /**
55
+ * Profiles to iterate. Default: `FLEET_VIEWPORT_PROFILES`. Pass an
56
+ * empty array to run only the currently-set page viewport.
57
+ */
58
+ profiles?: DeviceProfile[];
59
+ /**
60
+ * Extra time (ms) to settle after navigation before reading the
61
+ * captured warnings. Default 400 — gives the primitive's two
62
+ * `requestAnimationFrame` ticks plus a buffer for `env()` to apply.
63
+ */
64
+ settleMs?: number;
65
+ /**
66
+ * Additional console-message predicate. Default matches anything
67
+ * starting with `[@etamong-playground/ui]`.
68
+ */
69
+ predicate?: (msg: ConsoleMessage) => boolean;
70
+ }
71
+ /**
72
+ * Open each URL at each profile and assert no chrome primitive emitted
73
+ * a `[@etamong-playground/ui]` warning. The first violation throws with the
74
+ * original message — the failing test's stack carries the URL + profile.
75
+ */
76
+ declare function assertViewportFit(page: Page, urls: string[], options?: AssertViewportFitOptions): Promise<void>;
77
+ /**
78
+ * Convenience: a Playwright test that wraps `assertViewportFit` and
79
+ * fans out across profiles as separate test cases (so the failure
80
+ * report names the profile). Apps wire this in their `tests/` dir as:
81
+ *
82
+ * ```ts
83
+ * import { defineViewportFitTests } from "@etamong-playground/ui/testing";
84
+ * defineViewportFitTests({
85
+ * urls: ["/", "/settings", "/me"],
86
+ * beforeEach: async ({ page }) => { /* auth, mocks */ interface DefineViewportFitTestsOptions {
87
+ urls: string[];
88
+ profiles?: DeviceProfile[];
89
+ beforeEach?: (args: PlaywrightTestArgs) => Promise<void> | void;
90
+ /** Optional title prefix. Default: "viewport-fit". */
91
+ titlePrefix?: string;
92
+ }
93
+ declare function defineViewportFitTests(test: any, options: DefineViewportFitTestsOptions): void;
94
+
95
+ /**
96
+ * Cross-app MSW helpers. Optional — apps that use a different mock layer
97
+ * don't pull `msw` in (it's a peer dep).
98
+ *
99
+ * Two contracts every etamong-lab webui shares:
100
+ *
101
+ * 1. `/me` from oauth2-proxy/Authentik returns `{email, is_admin, ...}`.
102
+ * 2. Errors carry `{error, ref}` per `@etamong-lab/httperr`. Tests want a
103
+ * one-liner to produce a realistic failure.
104
+ *
105
+ * Both live here so a per-app `handlers.ts` only encodes the app's own
106
+ * surface, not these standard shapes.
107
+ */
108
+
109
+ /** Random 8-hex ref, matching the httperr server-side format. */
110
+ declare function mockHttperrRef(): string;
111
+ /**
112
+ * httperr-shaped error body. Pass to `HttpResponse.json` with the desired
113
+ * status so MSW returns the same shape as the real apiserver.
114
+ */
115
+ declare function httperrBody(message: string, ref?: string): {
116
+ error: string;
117
+ ref: string;
118
+ };
119
+ interface MockMe {
120
+ email?: string;
121
+ is_admin?: boolean;
122
+ via_token?: boolean;
123
+ can_create_apps?: boolean;
124
+ }
125
+ /**
126
+ * `/me` handler at the standard fleet path. Pass any subset of fields; the
127
+ * rest fall back to sensible test defaults.
128
+ */
129
+ declare function createMeHandler(me?: MockMe, path?: string): HttpHandler;
130
+ /**
131
+ * `/me` that returns the oauth2-proxy "not signed in" 401, so the webui's
132
+ * sign-in flow renders.
133
+ */
134
+ declare function createMeSignedOutHandler(path?: string): HttpHandler;
135
+ /**
136
+ * Default healthz — every fleet apiserver exposes one and tests often hit it
137
+ * in setup teardown.
138
+ */
139
+ declare function createHealthzHandler(path?: string): HttpHandler;
140
+ /**
141
+ * Convenience bundle: `/me` (signed-in by default) + `/healthz`. The first
142
+ * call site usually wants both; pass overrides as needed.
143
+ */
144
+ declare function defaultMockHandlers(me?: MockMe, paths?: {
145
+ me?: string;
146
+ healthz?: string;
147
+ }): HttpHandler[];
148
+
149
+ interface FleetFixtures {
150
+ /** Hook called once per test; passed the page after locale/viewport setup. */
151
+ fleetPage: Page;
152
+ }
153
+ /**
154
+ * Extended Playwright `test` that:
155
+ * - sets `Accept-Language: ko-KR` and locale ko-KR (matches the production
156
+ * i18n default per planning concept fleet-language-policy)
157
+ * - sets timezone to Asia/Seoul so date assertions are stable
158
+ * - sets a 1280×800 desktop viewport unless the test overrides it
159
+ *
160
+ * Use:
161
+ * import { fleetTest as test } from "@etamong-playground/ui/testing";
162
+ * test("works", async ({ page }) => { ... });
163
+ */
164
+ declare const fleetTest: _playwright_test.TestType<PlaywrightTestArgs & _playwright_test.PlaywrightTestOptions & FleetFixtures, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
165
+
166
+ export { type AssertViewportFitOptions, type DefineViewportFitTestsOptions, type DeviceProfile, FLEET_VIEWPORT_PROFILES, type FleetFixtures, type MockMe, VIEWPORT_FIT_WARN_PREFIX, assertViewportFit, createHealthzHandler, createMeHandler, createMeSignedOutHandler, defaultMockHandlers, defineViewportFitTests, fleetTest, httperrBody, mockHttperrRef };
@@ -0,0 +1,173 @@
1
+ import { http, HttpResponse } from 'msw';
2
+ import { test } from '@playwright/test';
3
+
4
+ // src/testing-viewport-fit.ts
5
+ var VIEWPORT_FIT_WARN_PREFIX = "[@etamong-playground/ui]";
6
+ var FLEET_VIEWPORT_PROFILES = [
7
+ {
8
+ name: "iphone-17-pro-pwa",
9
+ viewport: { width: 402, height: 874 },
10
+ standalone: true,
11
+ safeAreaTop: 59,
12
+ safeAreaBottom: 34
13
+ },
14
+ {
15
+ name: "ipad-mini-portrait",
16
+ viewport: { width: 768, height: 1024 }
17
+ },
18
+ {
19
+ name: "desktop-1280",
20
+ viewport: { width: 1280, height: 800 }
21
+ },
22
+ {
23
+ name: "android-narrow",
24
+ viewport: { width: 360, height: 780 }
25
+ }
26
+ ];
27
+ async function assertViewportFit(page, urls, options = {}) {
28
+ const profiles = options.profiles && options.profiles.length ? options.profiles : FLEET_VIEWPORT_PROFILES;
29
+ const settleMs = options.settleMs ?? 400;
30
+ const predicate = options.predicate ?? ((msg) => msg.type() === "warning" && msg.text().startsWith(VIEWPORT_FIT_WARN_PREFIX));
31
+ const collected = [];
32
+ const onConsole = (msg) => {
33
+ if (predicate(msg)) collected.push(msg.text());
34
+ };
35
+ page.on("console", onConsole);
36
+ try {
37
+ for (const profile of profiles) {
38
+ await page.setViewportSize(profile.viewport);
39
+ if (profile.standalone) {
40
+ await page.emulateMedia({ media: "screen", colorScheme: "no-preference" });
41
+ await page.addInitScript(() => {
42
+ const origMatchMedia = window.matchMedia;
43
+ window.matchMedia = (q) => {
44
+ if (q.includes("display-mode: standalone")) {
45
+ return {
46
+ matches: true,
47
+ media: q,
48
+ onchange: null,
49
+ addListener: () => {
50
+ },
51
+ removeListener: () => {
52
+ },
53
+ addEventListener: () => {
54
+ },
55
+ removeEventListener: () => {
56
+ },
57
+ dispatchEvent: () => true
58
+ };
59
+ }
60
+ return origMatchMedia.call(window, q);
61
+ };
62
+ Object.defineProperty(navigator, "standalone", {
63
+ configurable: true,
64
+ value: true
65
+ });
66
+ });
67
+ }
68
+ if (profile.safeAreaTop != null || profile.safeAreaBottom != null) {
69
+ const top = profile.safeAreaTop ?? 0;
70
+ const bot = profile.safeAreaBottom ?? 0;
71
+ await page.addInitScript(
72
+ (insets) => {
73
+ const style = document.createElement("style");
74
+ style.textContent = `:root {
75
+ --etu-test-safe-top: ${insets.top}px;
76
+ --etu-test-safe-bot: ${insets.bot}px;
77
+ }
78
+ /* Stub env() so the browser sees a real inset value during
79
+ headless testing. CSS @supports cannot polyfill env(), so
80
+ apps that want this assertion to run under emulated
81
+ notched insets must use the .etu-test-safe-top/bot custom
82
+ properties instead of env() in tests \u2014 or apps can keep
83
+ env() and accept that desktop Chromium reports 0. */
84
+ `;
85
+ document.head.appendChild(style);
86
+ },
87
+ { top, bot }
88
+ );
89
+ }
90
+ for (const url of urls) {
91
+ collected.length = 0;
92
+ await page.goto(url, { waitUntil: "networkidle" });
93
+ await page.waitForTimeout(settleMs);
94
+ if (collected.length > 0) {
95
+ throw new Error(
96
+ `viewport-fit assertion failed at ${profile.name} on ${url}:
97
+ ` + collected.map((l) => " - " + l).join("\n")
98
+ );
99
+ }
100
+ }
101
+ }
102
+ } finally {
103
+ page.off("console", onConsole);
104
+ }
105
+ }
106
+ function defineViewportFitTests(test, options) {
107
+ const profiles = options.profiles && options.profiles.length ? options.profiles : FLEET_VIEWPORT_PROFILES;
108
+ const prefix = options.titlePrefix ?? "viewport-fit";
109
+ for (const profile of profiles) {
110
+ test(`${prefix} \u2014 ${profile.name}`, async (args) => {
111
+ if (options.beforeEach) await options.beforeEach(args);
112
+ await assertViewportFit(args.page, options.urls, {
113
+ profiles: [profile]
114
+ });
115
+ });
116
+ }
117
+ }
118
+ function mockHttperrRef() {
119
+ const b = new Uint8Array(4);
120
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
121
+ crypto.getRandomValues(b);
122
+ } else {
123
+ for (let i = 0; i < 4; i++) b[i] = Math.floor(Math.random() * 256);
124
+ }
125
+ return Array.from(b).map((x) => x.toString(16).padStart(2, "0")).join("");
126
+ }
127
+ function httperrBody(message, ref) {
128
+ return { error: message, ref: ref ?? mockHttperrRef() };
129
+ }
130
+ function createMeHandler(me = {}, path = "/api/v1/me") {
131
+ const body = {
132
+ email: me.email ?? "you@etamong.com",
133
+ is_admin: me.is_admin ?? false,
134
+ via_token: me.via_token ?? false,
135
+ can_create_apps: me.can_create_apps ?? false
136
+ };
137
+ return http.get(path, () => HttpResponse.json(body));
138
+ }
139
+ function createMeSignedOutHandler(path = "/api/v1/me") {
140
+ return http.get(
141
+ path,
142
+ () => HttpResponse.json(httperrBody("not signed in"), { status: 401 })
143
+ );
144
+ }
145
+ function createHealthzHandler(path = "/healthz") {
146
+ return http.get(path, () => new HttpResponse("ok", { status: 200 }));
147
+ }
148
+ function defaultMockHandlers(me = {}, paths = {}) {
149
+ return [createMeHandler(me, paths.me), createHealthzHandler(paths.healthz)];
150
+ }
151
+ var fleetTest = test.extend({
152
+ // eslint-disable-next-line no-empty-pattern
153
+ fleetPage: async ({ page }, use) => {
154
+ await use(page);
155
+ },
156
+ page: async ({ browser }, use) => {
157
+ const context = await browser.newContext({
158
+ locale: "ko-KR",
159
+ timezoneId: "Asia/Seoul",
160
+ viewport: { width: 1280, height: 800 },
161
+ extraHTTPHeaders: { "Accept-Language": "ko-KR" }
162
+ });
163
+ const page = await context.newPage();
164
+ try {
165
+ await use(page);
166
+ } finally {
167
+ await page.close();
168
+ await context.close();
169
+ }
170
+ }
171
+ });
172
+
173
+ export { FLEET_VIEWPORT_PROFILES, VIEWPORT_FIT_WARN_PREFIX, assertViewportFit, createHealthzHandler, createMeHandler, createMeSignedOutHandler, defaultMockHandlers, defineViewportFitTests, fleetTest, httperrBody, mockHttperrRef };