@ahnseungchan/device-farm 0.0.0-stage → 0.1.0
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 +20 -0
- package/LICENSE +21 -0
- package/README.md +195 -2
- package/dist/bin.js +79 -0
- package/dist/chaos-CIB7G5X5.js +453 -0
- package/dist/chunk-3GCH6TN3.js +86 -0
- package/dist/chunk-4UOXXMXL.js +74 -0
- package/dist/chunk-7OJDY276.js +16 -0
- package/dist/chunk-AVECSGEH.js +1462 -0
- package/dist/chunk-AXQNUPGC.js +416 -0
- package/dist/chunk-F4TMB4WY.js +54 -0
- package/dist/chunk-F6PZOBU6.js +167 -0
- package/dist/chunk-JD2L6T7E.js +56 -0
- package/dist/chunk-JZDYF3HL.js +169 -0
- package/dist/chunk-NJNTEWXE.js +134 -0
- package/dist/chunk-NLX3V7ZJ.js +134 -0
- package/dist/chunk-OUNLQI2N.js +34 -0
- package/dist/chunk-P2M55S53.js +110 -0
- package/dist/chunk-PLMSQ5KF.js +173 -0
- package/dist/chunk-PYZWMVJ6.js +302 -0
- package/dist/chunk-PZZQWRSU.js +283 -0
- package/dist/chunk-Q6CXF3NJ.js +102 -0
- package/dist/chunk-S6GHOP4B.js +111 -0
- package/dist/chunk-SWW42KTA.js +223 -0
- package/dist/chunk-UAHWEUDI.js +68 -0
- package/dist/chunk-VGPCTMUA.js +258 -0
- package/dist/chunk-WTTCWFOX.js +161 -0
- package/dist/chunk-ZE2GGXFO.js +169 -0
- package/dist/device-explore-PHS6SVEJ.js +64 -0
- package/dist/device-run-XPNRYXEL.js +60 -0
- package/dist/explore-allowlist-F44YQDVK.js +44 -0
- package/dist/index.d.ts +192 -0
- package/dist/index.js +24 -0
- package/dist/init-QFFEOJWY.js +29 -0
- package/dist/policy-check-AUWGFNHZ.js +75 -0
- package/dist/policy-watch-QPHI2P7Z.js +66 -0
- package/dist/qa-run-5LLQMLAM.js +59 -0
- package/dist/record-EGHVQFR7.js +152 -0
- package/dist/serve-XT32MAEN.js +1687 -0
- package/dist/server-3NM7ODUG.js +251 -0
- package/dist/templates/env.example +48 -0
- package/dist/templates/farm.config.ts +104 -0
- package/dist/web/assets/index-BCuSKs27.js +17 -0
- package/dist/web/assets/index-C3is0x-E.css +1 -0
- package/dist/web/favicon.svg +1 -0
- package/dist/web/index.html +14 -0
- package/package.json +95 -4
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import {
|
|
2
|
+
homeAttempts,
|
|
3
|
+
markStaleScript,
|
|
4
|
+
samePath,
|
|
5
|
+
staleMarkKey
|
|
6
|
+
} from "./chunk-NLX3V7ZJ.js";
|
|
7
|
+
|
|
8
|
+
// packages/devices/src/exec.ts
|
|
9
|
+
import { execFile } from "child_process";
|
|
10
|
+
var execFileText = (command, args) => new Promise((resolve, reject) => {
|
|
11
|
+
execFile(command, [...args], { maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
12
|
+
if (!error) {
|
|
13
|
+
resolve(stdout);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const firstLine = String(stderr).split("\n").find((line) => line.trim()) ?? error.message;
|
|
17
|
+
reject(new Error(firstLine));
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// packages/devices/src/android-driver.ts
|
|
22
|
+
import { execFile as execFile2 } from "child_process";
|
|
23
|
+
import { promisify } from "util";
|
|
24
|
+
import { setTimeout as wait2 } from "timers/promises";
|
|
25
|
+
import { chromium } from "playwright";
|
|
26
|
+
|
|
27
|
+
// packages/core/src/device/deep-link.ts
|
|
28
|
+
function deepLinkUrl(path, deepLink) {
|
|
29
|
+
if (!deepLink.scheme) throw new Error("\uC571 \uC124\uC815\uC5D0 deepLink.scheme\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. farm.config.ts\uC5D0 \uB525\uB9C1\uD06C \uC2A4\uD0B4\uC744 \uB123\uC5B4 \uC8FC\uC138\uC694.");
|
|
30
|
+
const trimmed = path.replace(/^\/+/, "");
|
|
31
|
+
if (trimmed === "" || trimmed.startsWith("?")) return `${deepLink.scheme}://${deepLink.homeHost}${trimmed}`;
|
|
32
|
+
return `${deepLink.scheme}://${trimmed}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// packages/devices/src/ios-driver.ts
|
|
36
|
+
import { setTimeout as wait } from "timers/promises";
|
|
37
|
+
var nativeContext = "NATIVE_APP";
|
|
38
|
+
var webViewPrefix = "WEBVIEW";
|
|
39
|
+
var textPollMs = 250;
|
|
40
|
+
var contextWaitMs = 1e4;
|
|
41
|
+
var stackSettleMs = 600;
|
|
42
|
+
var hasTextScript = "var squash = function (v) { return String(v).replace(/\\s+/g, ' ').trim(); }; return squash(document.body && document.body.innerText || '').includes(squash(arguments[0]));";
|
|
43
|
+
var clickTargetHelpers = `const nameOf = (el) => {
|
|
44
|
+
const label = el.getAttribute("aria-label");
|
|
45
|
+
if (label) return label.trim();
|
|
46
|
+
const ids = (el.getAttribute("aria-labelledby") || "").split(/\\s+/).filter(Boolean);
|
|
47
|
+
if (ids.length) return ids.map((id) => (document.getElementById(id) || {}).textContent || "").join(" ").trim();
|
|
48
|
+
return (el.innerText || el.textContent || "").trim();
|
|
49
|
+
};
|
|
50
|
+
const selectors = { button: 'button, [role="button"], input[type="button"], input[type="submit"]', link: 'a[href], [role="link"]' };`;
|
|
51
|
+
var clickButtonScript = `
|
|
52
|
+
// \uC904\uBC14\uAFC8\xB7\uC5F0\uC18D \uACF5\uBC31\uC740 \uD55C \uCE78\uC73C\uB85C \uBCF8\uB2E4(\u300C1\uC77C\uCC28\u21B5\uC624\uB298\u300D). \uBC84\uD2BC \uBAA9\uB85D(explore listActionsScript)\uACFC \uAC19\uC740 \uADDC\uCE59.
|
|
53
|
+
const squash = (value) => String(value || "").replace(/\\s+/g, " ").trim();
|
|
54
|
+
const name = squash(arguments[0]);
|
|
55
|
+
const role = arguments[1] || "button";
|
|
56
|
+
const prefix = arguments[2] === "prefix";
|
|
57
|
+
const matches = (value) => (prefix ? value.startsWith(name) : value === name);
|
|
58
|
+
${clickTargetHelpers}
|
|
59
|
+
const candidates = document.querySelectorAll(selectors[role]);
|
|
60
|
+
// \uAC19\uC740 \uC774\uB984\uC758 \uC228\uC740 \uBC84\uD2BC(\uC811\uD78C \uD0ED\xB7\uCE90\uB7EC\uC140 \uBCF5\uC81C\uBCF8)\uC744 \uB204\uB974\uC9C0 \uC54A\uAC8C \uD654\uBA74\uC5D0 \uADF8\uB824\uC9C4 \uAC83\uB9CC \uBCF8\uB2E4.
|
|
61
|
+
const visible = (el) => el.getClientRects().length > 0 && getComputedStyle(el).visibility !== "hidden";
|
|
62
|
+
// \uAC19\uC740 \uC774\uB984\uC774 \uC5EC\uB7FF\uC774\uBA74 \uB9C8\uC9C0\uB9C9(\uAC00\uC7A5 \uC704\uC5D0 \uB72C \uD31D\uC5C5\xB7\uC2DC\uD2B8\uB294 body \uB05D\uC5D0 \uBD99\uB294\uB2E4). \uC608: \uD480\uD31D\uC5C5 \u300C\uCDA9\uC804 \uCDE8\uC18C\uD558\uAE30\u300D CTA\uC640 \uD655\uC778 \uD31D\uC5C5 \uBC84\uD2BC.
|
|
63
|
+
const target = Array.from(candidates).filter((el) => visible(el) && matches(squash(el.tagName === "INPUT" ? el.value : nameOf(el)))).at(-1);
|
|
64
|
+
if (!target) return false;
|
|
65
|
+
target.click();
|
|
66
|
+
return true;
|
|
67
|
+
`;
|
|
68
|
+
function contextName(context) {
|
|
69
|
+
if (typeof context === "string") return context;
|
|
70
|
+
if (typeof context === "object" && context !== null && "id" in context && typeof context.id === "string") return context.id;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
async function findWebView(session, except = []) {
|
|
74
|
+
const deadline = Date.now() + contextWaitMs;
|
|
75
|
+
let latest;
|
|
76
|
+
while (Date.now() < deadline) {
|
|
77
|
+
const webViews = (await session.getContexts()).map(contextName).filter((name) => Boolean(name?.startsWith(webViewPrefix)));
|
|
78
|
+
latest = webViews.at(-1);
|
|
79
|
+
const fresh = webViews.filter((name) => !except.includes(name)).at(-1);
|
|
80
|
+
if (fresh) return fresh;
|
|
81
|
+
if (latest && except.length === 0) return latest;
|
|
82
|
+
await wait(textPollMs);
|
|
83
|
+
}
|
|
84
|
+
if (latest) return latest;
|
|
85
|
+
throw new Error("iOS \uC571\uC5D0\uC11C WebView \uCEE8\uD14D\uC2A4\uD2B8\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.");
|
|
86
|
+
}
|
|
87
|
+
async function webViewNames(session) {
|
|
88
|
+
return (await session.getContexts()).map(contextName).filter((name) => Boolean(name?.startsWith(webViewPrefix)));
|
|
89
|
+
}
|
|
90
|
+
function openIosDriver(session, appId, app) {
|
|
91
|
+
const { homeTabName, inPageBackNames } = app;
|
|
92
|
+
const resetStack = async () => {
|
|
93
|
+
if (app.webview.stackResetScript) await session.execute(`${app.webview.stackResetScript}; return true;`).catch(() => void 0);
|
|
94
|
+
};
|
|
95
|
+
const isTabRoot = (path) => path === "/" || Object.values(app.tabPaths).includes(path);
|
|
96
|
+
const inWebView = async () => {
|
|
97
|
+
await session.switchContext(await findWebView(session));
|
|
98
|
+
};
|
|
99
|
+
const leaveInPageScreen = async () => {
|
|
100
|
+
await inWebView().catch(() => void 0);
|
|
101
|
+
for (const name of inPageBackNames) {
|
|
102
|
+
if (await session.execute(clickButtonScript, name, "button").catch(() => false) === true) break;
|
|
103
|
+
}
|
|
104
|
+
await wait(stackSettleMs);
|
|
105
|
+
};
|
|
106
|
+
const hasText = async (text) => {
|
|
107
|
+
await inWebView();
|
|
108
|
+
return Boolean(await session.execute(hasTextScript, text));
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
app,
|
|
112
|
+
navigate: async (path) => {
|
|
113
|
+
await inWebView();
|
|
114
|
+
await resetStack();
|
|
115
|
+
await wait(stackSettleMs);
|
|
116
|
+
await session.execute(`${markStaleScript}; return true;`).catch(() => void 0);
|
|
117
|
+
const before = await webViewNames(session);
|
|
118
|
+
await session.execute("mobile: deepLink", { url: deepLinkUrl(path, app.deepLink), bundleId: appId });
|
|
119
|
+
await session.switchContext(await findWebView(session, isTabRoot(path) ? [] : before));
|
|
120
|
+
},
|
|
121
|
+
clickButton: async (name, role = "button", match = "exact") => {
|
|
122
|
+
await inWebView();
|
|
123
|
+
const clicked = await session.execute(clickButtonScript, name, role, match);
|
|
124
|
+
if (clicked !== true) throw new Error(`"${name}" ${role === "link" ? "\uB9C1\uD06C\uB97C" : "\uBC84\uD2BC\uC744"} \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.`);
|
|
125
|
+
},
|
|
126
|
+
goHome: async () => {
|
|
127
|
+
let lastError;
|
|
128
|
+
for (let attempt = 0; attempt < homeAttempts; attempt += 1) {
|
|
129
|
+
try {
|
|
130
|
+
for (const name of await webViewNames(session)) {
|
|
131
|
+
await session.switchContext(name);
|
|
132
|
+
await resetStack();
|
|
133
|
+
}
|
|
134
|
+
await wait(stackSettleMs);
|
|
135
|
+
await session.switchContext(await findWebView(session));
|
|
136
|
+
const clicked = await session.execute(clickButtonScript, homeTabName, "link");
|
|
137
|
+
if (clicked !== true) throw new Error(`\uD558\uB2E8 \u300C${homeTabName}\u300D \uD0ED\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.`);
|
|
138
|
+
return;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
lastError = error;
|
|
141
|
+
await leaveInPageScreen();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw lastError;
|
|
145
|
+
},
|
|
146
|
+
expectText: async (text, timeoutMs) => {
|
|
147
|
+
const deadline = Date.now() + timeoutMs;
|
|
148
|
+
while (!await hasText(text)) {
|
|
149
|
+
if (Date.now() >= deadline) throw new Error(`"${text}" \uBB38\uAD6C\uAC00 ${timeoutMs}ms \uC548\uC5D0 \uB098\uC624\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`);
|
|
150
|
+
await wait(textPollMs);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
currentPath: async () => {
|
|
154
|
+
await inWebView();
|
|
155
|
+
return new URL(await session.getUrl()).pathname;
|
|
156
|
+
},
|
|
157
|
+
hasText,
|
|
158
|
+
screenshot: async () => {
|
|
159
|
+
await session.switchContext(nativeContext);
|
|
160
|
+
return Buffer.from(await session.takeScreenshot(), "base64");
|
|
161
|
+
},
|
|
162
|
+
evaluate: async (script) => {
|
|
163
|
+
await inWebView();
|
|
164
|
+
return await session.execute(`return (${script});`);
|
|
165
|
+
},
|
|
166
|
+
close: async () => {
|
|
167
|
+
await session.switchContext(nativeContext);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// packages/devices/src/android-driver.ts
|
|
173
|
+
var devtoolsWaitMs = 3e4;
|
|
174
|
+
var pageWaitMs = 1e4;
|
|
175
|
+
var pollMs = 250;
|
|
176
|
+
var clickMarkAttribute = "data-device-farm-click";
|
|
177
|
+
var markButtonScript = clickButtonScript.replace(
|
|
178
|
+
"target.click();",
|
|
179
|
+
`document.querySelectorAll("[${clickMarkAttribute}]").forEach((el) => el.removeAttribute("${clickMarkAttribute}")); target.setAttribute("${clickMarkAttribute}", "");`
|
|
180
|
+
);
|
|
181
|
+
if (markButtonScript === clickButtonScript) throw new Error("clickButtonScript\uC5D0 target.click()\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
182
|
+
var clickWaitMs = 1e4;
|
|
183
|
+
var stackSettleMs2 = 600;
|
|
184
|
+
var execFileBuffer = promisify(execFile2);
|
|
185
|
+
var webViewBoundsPattern = /class="android\.webkit\.WebView"[^>]*bounds="\[(\d+),(\d+)\]/;
|
|
186
|
+
async function hideKeyboard(exec, serial) {
|
|
187
|
+
const state = await exec("adb", ["-s", serial, "shell", "dumpsys", "input_method"]).catch(() => "");
|
|
188
|
+
if (!state.includes("mInputShown=true")) return;
|
|
189
|
+
await exec("adb", ["-s", serial, "shell", "input", "keyevent", "KEYCODE_BACK"]);
|
|
190
|
+
await wait2(stackSettleMs2);
|
|
191
|
+
}
|
|
192
|
+
var nativeBackAttempts = 3;
|
|
193
|
+
function isAppNativeScreen(windowDump, appId, mainActivity) {
|
|
194
|
+
const focus = /mCurrentFocus=Window\{[^}]* ([\w.]+)\/([\w.$]+)\}/.exec(windowDump);
|
|
195
|
+
return focus !== null && focus[1] === appId && focus[2] !== mainActivity;
|
|
196
|
+
}
|
|
197
|
+
async function leaveNativeScreens(exec, serial, appId, mainActivity) {
|
|
198
|
+
if (!mainActivity) return;
|
|
199
|
+
for (let attempt = 0; attempt < nativeBackAttempts; attempt += 1) {
|
|
200
|
+
const dump = await exec("adb", ["-s", serial, "shell", "dumpsys", "window"]).catch(() => "");
|
|
201
|
+
if (!isAppNativeScreen(dump, appId, mainActivity)) return;
|
|
202
|
+
await exec("adb", ["-s", serial, "shell", "input", "keyevent", "KEYCODE_BACK"]);
|
|
203
|
+
await wait2(stackSettleMs2);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
var adValidatorDismissPattern = /text="Dismiss"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/;
|
|
207
|
+
var adValidatorTopRatio = 0.6;
|
|
208
|
+
var linkTapHeightRatio = 0.75;
|
|
209
|
+
var linkArriveWaitMs = 5e3;
|
|
210
|
+
var linkTapAttempts = 2;
|
|
211
|
+
var sheetCloseAttempts = 3;
|
|
212
|
+
async function dumpScreen(exec, serial, allWindows) {
|
|
213
|
+
const dumpPath = "/sdcard/device-farm-ui.xml";
|
|
214
|
+
await exec("adb", ["-s", serial, "shell", "uiautomator", "dump", ...allWindows ? ["--windows"] : [], dumpPath]);
|
|
215
|
+
return exec("adb", ["-s", serial, "shell", "cat", dumpPath]);
|
|
216
|
+
}
|
|
217
|
+
function webViewOrigin(screen) {
|
|
218
|
+
const match = webViewBoundsPattern.exec(screen);
|
|
219
|
+
if (!match) throw new Error("\uD654\uBA74\uC5D0\uC11C WebView \uC704\uCE58\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4(uiautomator).");
|
|
220
|
+
return { x: Number(match[1]), y: Number(match[2]) };
|
|
221
|
+
}
|
|
222
|
+
async function dismissAdValidator(exec, serial, screen) {
|
|
223
|
+
const match = adValidatorDismissPattern.exec(screen);
|
|
224
|
+
if (!match) return;
|
|
225
|
+
const [left, top, right, bottom] = match.slice(1).map(Number);
|
|
226
|
+
await exec("adb", ["-s", serial, "shell", "input", "tap", String(Math.round((left + right) / 2)), String(Math.round((top + bottom) / 2))]);
|
|
227
|
+
await wait2(stackSettleMs2);
|
|
228
|
+
}
|
|
229
|
+
async function devtoolsSocket(exec, serial, appId) {
|
|
230
|
+
const deadline = Date.now() + devtoolsWaitMs;
|
|
231
|
+
while (Date.now() < deadline) {
|
|
232
|
+
const pid = (await exec("adb", ["-s", serial, "shell", "pidof", appId]).catch(() => "")).trim().split(/\s+/)[0];
|
|
233
|
+
const name = `webview_devtools_remote_${pid}`;
|
|
234
|
+
if (pid && (await exec("adb", ["-s", serial, "shell", "cat", "/proc/net/unix"])).includes(`@${name}`)) return name;
|
|
235
|
+
await wait2(pollMs);
|
|
236
|
+
}
|
|
237
|
+
throw new Error(`${appId} WebView \uB514\uBC84\uAE45 \uC18C\uCF13\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. WebView \uB514\uBC84\uAE45\uC744 \uCF20 \uBE4C\uB4DC\uC778\uC9C0 \uD655\uC778\uD558\uC138\uC694.`);
|
|
238
|
+
}
|
|
239
|
+
async function connect(exec, serial, appId) {
|
|
240
|
+
const socket = await devtoolsSocket(exec, serial, appId);
|
|
241
|
+
const port = (await exec("adb", ["-s", serial, "forward", "tcp:0", `localabstract:${socket}`])).trim();
|
|
242
|
+
return chromium.connectOverCDP(`http://127.0.0.1:${port}`);
|
|
243
|
+
}
|
|
244
|
+
function appPages(context, origin) {
|
|
245
|
+
return context.pages().filter((page) => !page.isClosed() && page.url().startsWith(origin));
|
|
246
|
+
}
|
|
247
|
+
var pathOf = (page) => new URL(page.url()).pathname;
|
|
248
|
+
function createPageTracker(context, origin) {
|
|
249
|
+
let pinned = null;
|
|
250
|
+
let known = new Set(context.pages());
|
|
251
|
+
const pin = (page) => {
|
|
252
|
+
pinned = page;
|
|
253
|
+
known = new Set(context.pages());
|
|
254
|
+
return page;
|
|
255
|
+
};
|
|
256
|
+
const current = () => {
|
|
257
|
+
const pages = appPages(context, origin);
|
|
258
|
+
const fresh = pages.filter((page) => !known.has(page)).at(-1);
|
|
259
|
+
if (fresh) return pin(fresh);
|
|
260
|
+
if (pinned && !pinned.isClosed()) return pinned;
|
|
261
|
+
const last = pages.at(-1);
|
|
262
|
+
if (last) return pin(last);
|
|
263
|
+
throw new Error("Android \uC571\uC5D0\uC11C \uC6F9 \uD654\uBA74\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uB85C\uADF8\uC544\uC6C3\uB3FC \uB124\uC774\uD2F0\uBE0C \uB85C\uADF8\uC778 \uD654\uBA74\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
264
|
+
};
|
|
265
|
+
const arrive = async (path, waitMs = pageWaitMs) => {
|
|
266
|
+
const deadline = Date.now() + waitMs;
|
|
267
|
+
while (Date.now() < deadline) {
|
|
268
|
+
const pages = appPages(context, origin);
|
|
269
|
+
const fresh = pages.filter((page) => !known.has(page)).at(-1);
|
|
270
|
+
if (fresh) return pin(fresh);
|
|
271
|
+
const tab = pages.find((page) => samePath(path, pathOf(page)));
|
|
272
|
+
if (tab) {
|
|
273
|
+
await tab.evaluate(`delete window.${staleMarkKey}`).catch(() => void 0);
|
|
274
|
+
return pin(tab);
|
|
275
|
+
}
|
|
276
|
+
await wait2(pollMs);
|
|
277
|
+
}
|
|
278
|
+
return current();
|
|
279
|
+
};
|
|
280
|
+
const forget = () => {
|
|
281
|
+
known = new Set(context.pages());
|
|
282
|
+
};
|
|
283
|
+
return { current, arrive, forget };
|
|
284
|
+
}
|
|
285
|
+
async function openAndroidDriver(serial, appId, webOrigin, app, exec = execFileText) {
|
|
286
|
+
const { homeTabName, inPageBackNames } = app;
|
|
287
|
+
const { stackResetScript, sheetCloseName } = app.webview;
|
|
288
|
+
const browser = await connect(exec, serial, appId);
|
|
289
|
+
const context = browser.contexts()[0];
|
|
290
|
+
if (!context) {
|
|
291
|
+
await browser.close();
|
|
292
|
+
throw new Error("Android WebView \uCEE8\uD14D\uC2A4\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
293
|
+
}
|
|
294
|
+
const origin = new URL(webOrigin).origin;
|
|
295
|
+
const deadline = Date.now() + pageWaitMs;
|
|
296
|
+
while (appPages(context, origin).length === 0 && Date.now() < deadline) await wait2(pollMs);
|
|
297
|
+
const pages = createPageTracker(context, origin);
|
|
298
|
+
const squash = (value) => value.replace(/\s+/g, " ").trim();
|
|
299
|
+
const hasText = async (text) => squash(await pages.current().locator("body").innerText()).includes(squash(text));
|
|
300
|
+
let webViewTopLeft = null;
|
|
301
|
+
const locate = async (page, name, role, match = "exact") => {
|
|
302
|
+
const script = `(function () {
|
|
303
|
+
if (!(function () { ${markButtonScript} }).apply(null, [${JSON.stringify(name)}, ${JSON.stringify(role)}, ${JSON.stringify(match)}])) return null;
|
|
304
|
+
const el = document.querySelector("[${clickMarkAttribute}]");
|
|
305
|
+
// \uAC00\uC6B4\uB370\uB85C \uC62C\uB9B0\uB2E4. \uD654\uBA74 \uC544\uB798 \uB05D\uC5D0 \uB450\uBA74 \uB5A0 \uC788\uB294 \uD558\uC704 \uBA54\uB274 \uBC11\uC5D0 \uAE54\uB824 \uD0ED\uC774 \uADF8\uCABD\uC5D0 \uAC04\uB2E4.
|
|
306
|
+
el.scrollIntoView({ block: "center", inline: "nearest" });
|
|
307
|
+
const rect = el.getBoundingClientRect();
|
|
308
|
+
return { x: rect.x + rect.width / 2, y: rect.y + rect.height * ${role === "link" ? linkTapHeightRatio : 0.5}, ratio: window.devicePixelRatio, viewHeight: window.innerHeight, href: el.getAttribute("href") };
|
|
309
|
+
})()`;
|
|
310
|
+
return page.evaluate(script).catch(() => null);
|
|
311
|
+
};
|
|
312
|
+
const tapAt = async (point, forcePopupCheck) => {
|
|
313
|
+
const inPopupArea = point.y > point.viewHeight * adValidatorTopRatio;
|
|
314
|
+
webViewTopLeft ??= webViewOrigin(await dumpScreen(exec, serial, false));
|
|
315
|
+
if (inPopupArea || forcePopupCheck) {
|
|
316
|
+
const screen = await dumpScreen(exec, serial, true).catch(() => "");
|
|
317
|
+
await dismissAdValidator(exec, serial, screen);
|
|
318
|
+
}
|
|
319
|
+
const x = Math.round(webViewTopLeft.x + point.x * point.ratio);
|
|
320
|
+
const y = Math.round(webViewTopLeft.y + point.y * point.ratio);
|
|
321
|
+
await exec("adb", ["-s", serial, "shell", "input", "tap", String(x), String(y)]);
|
|
322
|
+
};
|
|
323
|
+
const followLink = async (point, name, path) => {
|
|
324
|
+
for (let attempt = 0; attempt < linkTapAttempts; attempt += 1) {
|
|
325
|
+
if (attempt > 0) await tapAt(point, true);
|
|
326
|
+
if (samePath(path, pathOf(await pages.arrive(path, linkArriveWaitMs)))) return;
|
|
327
|
+
}
|
|
328
|
+
throw new Error(`\u300C${name}\u300D \uB9C1\uD06C\uB97C \uB20C\uB800\uC9C0\uB9CC ${path}\uB85C \uAC00\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`);
|
|
329
|
+
};
|
|
330
|
+
const closeSheets = async () => {
|
|
331
|
+
for (let attempt = 0; attempt < sheetCloseAttempts; attempt += 1) {
|
|
332
|
+
const point = await locate(pages.current(), sheetCloseName, "button");
|
|
333
|
+
if (!point) return;
|
|
334
|
+
await tapAt(point, false);
|
|
335
|
+
await wait2(stackSettleMs2);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const leaveInPageScreen = async () => {
|
|
339
|
+
for (const name of inPageBackNames) {
|
|
340
|
+
const point = await locate(pages.current(), name, "button");
|
|
341
|
+
if (!point) continue;
|
|
342
|
+
await tapAt(point, false);
|
|
343
|
+
await wait2(stackSettleMs2);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const clickButton = async (name, role = "button", match = "exact") => {
|
|
348
|
+
const deadline2 = Date.now() + clickWaitMs;
|
|
349
|
+
for (; ; ) {
|
|
350
|
+
const page = pages.current();
|
|
351
|
+
const point = await locate(page, name, role, match);
|
|
352
|
+
if (point) {
|
|
353
|
+
await tapAt(point, false);
|
|
354
|
+
if (point.href) await followLink(point, name, new URL(point.href, page.url()).pathname);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (Date.now() >= deadline2) throw new Error(`"${name}" ${role === "link" ? "\uB9C1\uD06C\uB97C" : "\uBC84\uD2BC\uC744"} \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.`);
|
|
358
|
+
await wait2(pollMs);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
const resetStack = (page) => stackResetScript ? page.evaluate(stackResetScript).catch(() => void 0) : Promise.resolve(void 0);
|
|
362
|
+
return {
|
|
363
|
+
app,
|
|
364
|
+
context,
|
|
365
|
+
navigate: async (path) => {
|
|
366
|
+
await hideKeyboard(exec, serial);
|
|
367
|
+
await resetStack(pages.current());
|
|
368
|
+
await wait2(stackSettleMs2);
|
|
369
|
+
await pages.current().evaluate(markStaleScript).catch(() => void 0);
|
|
370
|
+
pages.forget();
|
|
371
|
+
await exec("adb", ["-s", serial, "shell", "am", "start", "-W", "-a", "android.intent.action.VIEW", "-d", `'${deepLinkUrl(path, app.deepLink)}'`, appId]);
|
|
372
|
+
await pages.arrive(path);
|
|
373
|
+
},
|
|
374
|
+
clickButton,
|
|
375
|
+
goHome: async () => {
|
|
376
|
+
let lastError;
|
|
377
|
+
for (let attempt = 0; attempt < homeAttempts; attempt += 1) {
|
|
378
|
+
try {
|
|
379
|
+
await leaveNativeScreens(exec, serial, appId, app.android.mainActivity);
|
|
380
|
+
await hideKeyboard(exec, serial);
|
|
381
|
+
await Promise.all(appPages(context, origin).map(resetStack));
|
|
382
|
+
await wait2(stackSettleMs2);
|
|
383
|
+
pages.forget();
|
|
384
|
+
await closeSheets();
|
|
385
|
+
await clickButton(homeTabName, "link");
|
|
386
|
+
return;
|
|
387
|
+
} catch (error) {
|
|
388
|
+
lastError = error;
|
|
389
|
+
await leaveInPageScreen();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
throw lastError;
|
|
393
|
+
},
|
|
394
|
+
expectText: async (text, timeoutMs) => {
|
|
395
|
+
const deadline2 = Date.now() + timeoutMs;
|
|
396
|
+
while (!await hasText(text).catch(() => false)) {
|
|
397
|
+
if (Date.now() >= deadline2) throw new Error(`"${text}" \uBB38\uAD6C\uAC00 ${timeoutMs}ms \uC548\uC5D0 \uB098\uC624\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`);
|
|
398
|
+
await wait2(pollMs);
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
currentPath: async () => pathOf(pages.current()),
|
|
402
|
+
hasText,
|
|
403
|
+
screenshot: async () => (await execFileBuffer("adb", ["-s", serial, "exec-out", "screencap", "-p"], { encoding: "buffer", maxBuffer: 64 * 1024 * 1024 })).stdout,
|
|
404
|
+
evaluate: async (script) => pages.current().evaluate(script),
|
|
405
|
+
close: () => browser.close()
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export {
|
|
410
|
+
execFileText,
|
|
411
|
+
nativeContext,
|
|
412
|
+
clickTargetHelpers,
|
|
413
|
+
openIosDriver,
|
|
414
|
+
appPages,
|
|
415
|
+
openAndroidDriver
|
|
416
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizePath
|
|
3
|
+
} from "./chunk-NLX3V7ZJ.js";
|
|
4
|
+
|
|
5
|
+
// packages/engine/src/screen-map-store.ts
|
|
6
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
// packages/core/src/device/screen-map.ts
|
|
10
|
+
function edgeKind(result) {
|
|
11
|
+
if (result.status === "skipped") return null;
|
|
12
|
+
if (result.status === "reacted") return result.to ? "navigate" : "popup";
|
|
13
|
+
return result.status;
|
|
14
|
+
}
|
|
15
|
+
function edgesFrom(from, results, seenAt) {
|
|
16
|
+
return results.flatMap((result) => {
|
|
17
|
+
const kind = edgeKind(result);
|
|
18
|
+
if (!kind) return [];
|
|
19
|
+
const edge = { from: normalizePath(from), role: result.role, name: result.name, kind, seenAt };
|
|
20
|
+
if (result.to) edge.to = normalizePath(result.to);
|
|
21
|
+
if (result.texts && result.texts.length > 0) edge.texts = result.texts;
|
|
22
|
+
if (kind === "error" && result.message) edge.message = result.message;
|
|
23
|
+
return [edge];
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
var edgeKey = (edge) => `${edge.from}\0${edge.role}\0${edge.name}`;
|
|
27
|
+
function mergeEdges(current, next) {
|
|
28
|
+
const merged = new Map(current.map((edge) => [edgeKey(edge), edge]));
|
|
29
|
+
for (const edge of next) merged.set(edgeKey(edge), edge);
|
|
30
|
+
return [...merged.values()].sort((a, b) => edgeKey(a) < edgeKey(b) ? -1 : edgeKey(a) > edgeKey(b) ? 1 : 0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// packages/engine/src/screen-map-store.ts
|
|
34
|
+
var screenMapFile = path.resolve("test-results/screen-map.json");
|
|
35
|
+
function readScreenMap(file = screenMapFile) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
38
|
+
} catch {
|
|
39
|
+
return { edges: [] };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function recordScreenMap(from, results, file = screenMapFile, now = /* @__PURE__ */ new Date()) {
|
|
43
|
+
const edges = mergeEdges(readScreenMap(file).edges, edgesFrom(from, results, now.toISOString()));
|
|
44
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
45
|
+
const temp = `${file}.tmp`;
|
|
46
|
+
writeFileSync(temp, `${JSON.stringify({ edges }, null, 2)}
|
|
47
|
+
`);
|
|
48
|
+
renameSync(temp, file);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
readScreenMap,
|
|
53
|
+
recordScreenMap
|
|
54
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// packages/core/src/policy/snapshot.ts
|
|
2
|
+
var emptySnapshot = { checkedAt: "", pages: {} };
|
|
3
|
+
function diffSnapshots(previous, next) {
|
|
4
|
+
const changes = [];
|
|
5
|
+
for (const page of Object.values(next.pages)) {
|
|
6
|
+
const before = previous.pages[page.id];
|
|
7
|
+
if (!before) changes.push({ kind: "added", page });
|
|
8
|
+
else if (before.hash !== page.hash) changes.push({ kind: "changed", page, before });
|
|
9
|
+
}
|
|
10
|
+
for (const page of Object.values(previous.pages)) {
|
|
11
|
+
if (!next.pages[page.id]) changes.push({ kind: "removed", page });
|
|
12
|
+
}
|
|
13
|
+
return changes.sort((a, b) => a.page.title.localeCompare(b.page.title, "ko"));
|
|
14
|
+
}
|
|
15
|
+
var recentPolicyDays = 14;
|
|
16
|
+
var dayMs = 864e5;
|
|
17
|
+
function isRecentlyChanged(page, now = Date.now()) {
|
|
18
|
+
const modified = Date.parse(page.modifiedAt);
|
|
19
|
+
return Number.isFinite(modified) && now - modified < recentPolicyDays * dayMs;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// packages/core/src/policy/rules.ts
|
|
23
|
+
var squash = (text) => text.replace(/\s+/g, "");
|
|
24
|
+
var entities = { nbsp: " ", amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
25
|
+
function storageToText(storage) {
|
|
26
|
+
return storage.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|h[1-6]|li|tr|div)>/gi, "\n").replace(/<\/(td|th)>/gi, " | ").replace(/<[^>]+>/g, "").replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (whole, code) => {
|
|
27
|
+
if (code.startsWith("#x") || code.startsWith("#X")) return String.fromCodePoint(parseInt(code.slice(2), 16));
|
|
28
|
+
if (code.startsWith("#")) return String.fromCodePoint(Number(code.slice(1)));
|
|
29
|
+
return entities[code.toLowerCase()] ?? whole;
|
|
30
|
+
}).replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
31
|
+
}
|
|
32
|
+
var numbersIn = (text) => text.match(/\d[\d,.]*/g)?.map((value) => value.replace(/[,.]$/, "")) ?? [];
|
|
33
|
+
function rejectReason(candidate, bodyText) {
|
|
34
|
+
const evidence = squash(candidate.evidence);
|
|
35
|
+
if (!evidence) return "\uADFC\uAC70 \uC778\uC6A9\uC774 \uBE44\uC5C8\uC2B5\uB2C8\uB2E4.";
|
|
36
|
+
if (!squash(bodyText).includes(evidence)) return "\uADFC\uAC70 \uC778\uC6A9\uC774 \uC815\uCC45 \uBCF8\uBB38\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4.";
|
|
37
|
+
if (candidate.kind === "copy" && (!candidate.expected || !evidence.includes(squash(candidate.expected)))) {
|
|
38
|
+
return "\uC548\uB0B4 \uBB38\uAD6C\uAC00 \uADFC\uAC70 \uC778\uC6A9\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4.";
|
|
39
|
+
}
|
|
40
|
+
if (candidate.kind === "value") {
|
|
41
|
+
const missing = numbersIn(candidate.expected ?? candidate.statement).filter((value) => !evidence.includes(squash(value)));
|
|
42
|
+
if (missing.length > 0) return `\uAC12 ${missing.join(", ")}\uC774 \uADFC\uAC70 \uC778\uC6A9\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4.`;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function screenMatches(pattern, path) {
|
|
47
|
+
const clean = (value) => value.split("?")[0].replace(/\/+$/, "") || "/";
|
|
48
|
+
const parts = clean(pattern).split("/");
|
|
49
|
+
const target = clean(path).split("/");
|
|
50
|
+
return parts.length === target.length && parts.every((part, index) => part === "*" || part === target[index]);
|
|
51
|
+
}
|
|
52
|
+
function shortHash(text) {
|
|
53
|
+
let hash = 2166136261;
|
|
54
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
55
|
+
hash ^= text.charCodeAt(index);
|
|
56
|
+
hash = Math.imul(hash, 16777619);
|
|
57
|
+
}
|
|
58
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
59
|
+
}
|
|
60
|
+
function ruleId(pageId, candidate) {
|
|
61
|
+
return `${pageId}-${candidate.kind}-${shortHash(`${squash(candidate.evidence)}\0${squash(candidate.expected ?? candidate.statement)}`)}`;
|
|
62
|
+
}
|
|
63
|
+
function buildPageRules(page, bodyText, candidates, previous) {
|
|
64
|
+
const sinceOf = new Map(previous.map((rule) => [rule.id, rule.since]));
|
|
65
|
+
const rules = /* @__PURE__ */ new Map();
|
|
66
|
+
const dropped = [];
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
const reason = rejectReason(candidate, bodyText);
|
|
69
|
+
if (reason) {
|
|
70
|
+
dropped.push({ statement: candidate.statement, reason });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const id = ruleId(page.id, candidate);
|
|
74
|
+
rules.set(id, { ...candidate, id, pageId: page.id, pageTitle: page.title, pageHash: page.hash, since: sinceOf.get(id) ?? page.modifiedAt });
|
|
75
|
+
}
|
|
76
|
+
return { rules: [...rules.values()], dropped };
|
|
77
|
+
}
|
|
78
|
+
var kinds = ["copy", "value", "behavior"];
|
|
79
|
+
var isString = (value) => typeof value === "string";
|
|
80
|
+
function toCandidate(value) {
|
|
81
|
+
if (!value || typeof value !== "object") return null;
|
|
82
|
+
const item = value;
|
|
83
|
+
if (!kinds.includes(item.kind) || !isString(item.statement) || !isString(item.evidence)) return null;
|
|
84
|
+
return {
|
|
85
|
+
kind: item.kind,
|
|
86
|
+
section: isString(item.section) ? item.section : "",
|
|
87
|
+
statement: item.statement,
|
|
88
|
+
expected: isString(item.expected) && item.expected.trim() ? item.expected : void 0,
|
|
89
|
+
evidence: item.evidence,
|
|
90
|
+
screens: Array.isArray(item.screens) ? item.screens.filter(isString) : [],
|
|
91
|
+
visibleOnOpen: item.visibleOnOpen === true
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function parseJsonObject(text) {
|
|
95
|
+
const start = text.indexOf("{");
|
|
96
|
+
const end = text.lastIndexOf("}");
|
|
97
|
+
if (start < 0 || end <= start) throw new Error("\uC751\uB2F5\uC5D0 JSON\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
98
|
+
const parsed = JSON.parse(text.slice(start, end + 1));
|
|
99
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("\uC751\uB2F5 JSON\uC774 \uAC1D\uCCB4\uAC00 \uC544\uB2D9\uB2C8\uB2E4.");
|
|
100
|
+
return parsed;
|
|
101
|
+
}
|
|
102
|
+
function parseExtraction(text) {
|
|
103
|
+
const parsed = parseJsonObject(text);
|
|
104
|
+
const screens = Array.isArray(parsed.screens) ? parsed.screens.filter(isString) : [];
|
|
105
|
+
const candidates = Array.isArray(parsed.rules) ? parsed.rules.map(toCandidate).filter((item) => item !== null) : [];
|
|
106
|
+
return { screens, candidates };
|
|
107
|
+
}
|
|
108
|
+
var maxPolicyChars = 6e4;
|
|
109
|
+
function extractPrompt(title, bodyText, knownScreens, appName = "\uB300\uC0C1") {
|
|
110
|
+
return [
|
|
111
|
+
`${appName} \uC571\uC758 \uC815\uCC45 \uBB38\uC11C \u300C${title}\u300D\uC5D0\uC11C \uC571 \uD654\uBA74\uC73C\uB85C \uD655\uC778\uD560 \uC218 \uC788\uB294 \uADDC\uCE59\uC744 \uBF51\uC544 JSON \uD558\uB098\uB85C\uB9CC \uB2F5\uD558\uC138\uC694.`,
|
|
112
|
+
"",
|
|
113
|
+
"\uADDC\uCE59 \uC885\uB958:",
|
|
114
|
+
'- "copy": \uACE0\uAC1D\uC5D0\uAC8C \uBCF4\uC774\uB294 \uC548\uB0B4 \uBB38\uAD6C. expected\uC5D0 \uBB38\uAD6C\uB97C \uC815\uCC45\uC5D0 \uC801\uD78C \uADF8\uB300\uB85C \uB123\uB294\uB2E4.',
|
|
115
|
+
'- "value": \uAE08\uC561\xB7\uD69F\uC218\xB7\uAE30\uAC04\xB7\uBE44\uC728\uCC98\uB7FC \uD654\uBA74\uC5D0 \uBCF4\uC774\uB294 \uAC12. expected\uC5D0 \uD654\uBA74\uC5D0 \uBCF4\uC77C \uAE00\uC790(\uC608: "1,000\uC6D0")\uB97C \uB123\uB294\uB2E4.',
|
|
116
|
+
'- "behavior": \uC0C1\uD0DC\uC5D0 \uB530\uB77C \uBC84\uD2BC\xB7\uBB38\uAD6C\uAC00 \uBCF4\uC774\uAC70\uB098 \uC228\uB294 \uAC83\uCC98\uB7FC \uD654\uBA74\uC744 \uBD10\uC57C \uD310\uB2E8\uD560 \uC218 \uC788\uB294 \uAC83.',
|
|
117
|
+
"",
|
|
118
|
+
"\uC9C0\uD0AC \uAC83:",
|
|
119
|
+
"- evidence\uC5D0\uB294 \uC815\uCC45 \uBCF8\uBB38\uC758 \uBB38\uC7A5\uC774\uB098 \uD45C \uD55C \uC904\uC744 \uD55C \uAE00\uC790\uB3C4 \uBC14\uAFB8\uC9C0 \uB9D0\uACE0 \uADF8\uB300\uB85C \uBCF5\uC0AC\uD55C\uB2E4. \uC694\uC57D\xB7\uBC88\uC5ED\xB7\uB744\uC5B4\uC4F0\uAE30 \uC218\uC815 \uAE08\uC9C0. \uBCF8\uBB38\uC5D0 \uC5C6\uB294 \uADDC\uCE59\uC740 \uB9CC\uB4E4\uC9C0 \uC54A\uB294\uB2E4.",
|
|
120
|
+
"- \uC5B4\uB4DC\uBBFC\uC5D0\uC11C \uBC14\uAFC0 \uC218 \uC788\uB2E4\uACE0 \uC801\uD78C \uAC12\uC740 value\uB85C \uBF51\uC9C0 \uC54A\uB294\uB2E4.",
|
|
121
|
+
"- screens\uB294 \uC544\uB798 \uD654\uBA74 \uBAA9\uB85D\uC5D0\uC11C\uB9CC \uACE0\uB978\uB2E4. \uACBD\uB85C \uD55C \uB9C8\uB514\uB97C *\uB85C \uC4F8 \uC218 \uC788\uB2E4. \uB9DE\uB294 \uD654\uBA74\uC774 \uC5C6\uC73C\uBA74 \uBE48 \uBC30\uC5F4.",
|
|
122
|
+
"- visibleOnOpen\uC740 \uADF8 \uD654\uBA74\uC744 \uC5F4\uAE30\uB9CC \uD574\uB3C4 \uBCF4\uC774\uBA74 true, \uD1A0\uC2A4\uD2B8\xB7\uBAA8\uB2EC\xB7\uC624\uB958\xB7\uD2B9\uC815 \uC0C1\uD0DC\uC5D0\uC11C\uB9CC \uBCF4\uC774\uBA74 false.",
|
|
123
|
+
"- \uCD5C\uC0C1\uC704 screens\uC5D0\uB294 \uC774 \uC815\uCC45\uC774 \uB2E4\uB8E8\uB294 \uD654\uBA74\uC744 \uB123\uB294\uB2E4.",
|
|
124
|
+
"",
|
|
125
|
+
'\uD615\uC2DD: {"screens": ["/path"], "rules": [{"kind": "copy", "section": "\uC139\uC158 \uC81C\uBAA9", "statement": "\uADDC\uCE59 \uD55C \uC904", "expected": "\uD654\uBA74 \uAE00\uC790", "evidence": "\uBCF8\uBB38 \uADF8\uB300\uB85C", "screens": ["/path"], "visibleOnOpen": true}]}',
|
|
126
|
+
"",
|
|
127
|
+
"\uD654\uBA74 \uBAA9\uB85D:",
|
|
128
|
+
...knownScreens.map((screen) => `- ${screen}`),
|
|
129
|
+
"",
|
|
130
|
+
"\uC815\uCC45 \uBCF8\uBB38:",
|
|
131
|
+
bodyText.slice(0, maxPolicyChars)
|
|
132
|
+
].join("\n");
|
|
133
|
+
}
|
|
134
|
+
var findingLabels = {
|
|
135
|
+
"pending-app": "\uC571 \uBC18\uC601 \uC804",
|
|
136
|
+
"app-changed": "\uBB38\uC11C \uAC31\uC2E0 \uB204\uB77D \uB610\uB294 \uBC84\uADF8",
|
|
137
|
+
"policy-outdated": "\uC815\uCC45 \uBB38\uC11C\uAC00 \uB4A4\uCC98\uC9D0",
|
|
138
|
+
"docs-mismatch": "\uBB38\uC11C\uB07C\uB9AC \uC5B4\uAE0B\uB0A8",
|
|
139
|
+
"bug-candidate": "\uBC84\uADF8 \uD6C4\uBCF4"
|
|
140
|
+
};
|
|
141
|
+
var dayMs2 = 864e5;
|
|
142
|
+
function classifyFinding(rule, screenChanged, now) {
|
|
143
|
+
const since = Date.parse(rule.since);
|
|
144
|
+
if (Number.isFinite(since) && now - since < recentPolicyDays * dayMs2) return "pending-app";
|
|
145
|
+
return screenChanged ? "app-changed" : "bug-candidate";
|
|
146
|
+
}
|
|
147
|
+
function classifyWithFigma(rule, figmaAgreesWithPolicy, screenChanged, now) {
|
|
148
|
+
if (figmaAgreesWithPolicy === null) return classifyFinding(rule, screenChanged, now);
|
|
149
|
+
if (!figmaAgreesWithPolicy) return "policy-outdated";
|
|
150
|
+
return classifyFinding(rule, false, now);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
emptySnapshot,
|
|
155
|
+
diffSnapshots,
|
|
156
|
+
isRecentlyChanged,
|
|
157
|
+
squash,
|
|
158
|
+
storageToText,
|
|
159
|
+
screenMatches,
|
|
160
|
+
shortHash,
|
|
161
|
+
buildPageRules,
|
|
162
|
+
parseJsonObject,
|
|
163
|
+
parseExtraction,
|
|
164
|
+
extractPrompt,
|
|
165
|
+
findingLabels,
|
|
166
|
+
classifyWithFigma
|
|
167
|
+
};
|