objs-core 2.3.0 → 2.4.1
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/README.md +572 -618
- package/objs-extension/README.md +32 -0
- package/objs-extension/background.js +110 -0
- package/objs-extension/bridge.js +193 -0
- package/objs-extension/icons/icon128.png +0 -0
- package/objs-extension/lib/objs-inject.js +5308 -0
- package/objs-extension/manifest.json +18 -0
- package/objs-extension/sidepanel.css +455 -0
- package/objs-extension/sidepanel.html +56 -0
- package/objs-extension/sidepanel.js +908 -0
- package/objs.built.js +475 -120
- package/objs.built.min.js +63 -54
- package/objs.d.ts +584 -525
- package/objs.global.js +5308 -0
- package/objs.global.min.js +98 -0
- package/objs.js +593 -134
- package/package.json +73 -70
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Objs Test Recorder (Chrome extension)
|
|
2
|
+
|
|
3
|
+
Manifest V3 extension bundled with **Objs 2.4**. It injects Objs into the **page MAIN world** (`lib/objs-inject.js` + `bridge.js`).
|
|
4
|
+
|
|
5
|
+
- **Tests** are stored as **JavaScript** from `o.exportTest()` (same idea as **Export Objs test** in the [recording example](../examples/recording/index.html)). Edit that script in the accordion textarea.
|
|
6
|
+
- **Play** runs the exported script with `o.test` (sync) or **legacy JSON** via `o.playRecording`. When a run finishes, the page shows the same **`o.testOverlay()`** panel as the [recording example](../examples/recording/index.html). The **Show succeeded** checkbox sets **`o.tShowOk`** before the run (whether passed steps get “OK” lines in the log / overlay).
|
|
7
|
+
- **Stop** after recording updates both the script and an internal snapshot for `recordingSnapshot` (Playwright export).
|
|
8
|
+
- Each test has **Observe root** (selector) and **`o.autotag`** (default `qa` → `data-qa`), applied on Record / Play in the page before Objs runs.
|
|
9
|
+
|
|
10
|
+
The Objs project does **not** publish this to the public Chrome Web Store. Package it for your org (zip, private store, policy).
|
|
11
|
+
|
|
12
|
+
## Load unpacked (development)
|
|
13
|
+
|
|
14
|
+
1. Run `npm run build` at the repo root so `lib/objs-inject.js` matches `objs.js`.
|
|
15
|
+
2. Open `chrome://extensions`, enable **Developer mode**, **Load unpacked**, select this **`objs-extension`** folder.
|
|
16
|
+
3. Open a normal **http(s)** tab. Open the extension **popup** — tab resolution uses the **browser** window, not the popup.
|
|
17
|
+
|
|
18
|
+
## Enterprise packaging
|
|
19
|
+
|
|
20
|
+
- Zip the contents of `objs-extension/` for internal distribution.
|
|
21
|
+
- Adjust `manifest.json` **host_permissions** if `<all_urls>` is too broad for your policy.
|
|
22
|
+
- Sign or deploy via Chrome policy / internal store per your IT guidelines.
|
|
23
|
+
|
|
24
|
+
## Files
|
|
25
|
+
|
|
26
|
+
| File | Role |
|
|
27
|
+
|------|------|
|
|
28
|
+
| `manifest.json` | MV3 manifest (`tabs` permission for reliable tab URL resolution) |
|
|
29
|
+
| `background.js` | Service worker: inject Objs, `getTargetTabId`, messaging |
|
|
30
|
+
| `sidepanel.html` / `sidepanel.js` | Accordion UI per test |
|
|
31
|
+
| `bridge.js` | `stopAndExport`, `runExportedTest`, `playRecording`, etc. |
|
|
32
|
+
| `lib/objs-inject.js` | Generated by `npm run build` — do not edit by hand |
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Objs Test Recorder — MV3 service worker
|
|
3
|
+
*
|
|
4
|
+
* UI opens via action.default_popup (sidepanel.html). No side_panel manifest key —
|
|
5
|
+
* that key is Chromium-version-specific and triggers “Unrecognized manifest key” in
|
|
6
|
+
* many browsers; popup works everywhere.
|
|
7
|
+
*
|
|
8
|
+
* Tab resolution: chrome.tabs.query({ active: true, lastFocusedWindow: true }) from the
|
|
9
|
+
* popup often returns no tab (the popup window has focus). Resolve the last-focused
|
|
10
|
+
* *normal* browser window’s active http(s) tab instead.
|
|
11
|
+
*/
|
|
12
|
+
async function getTargetTabId() {
|
|
13
|
+
const tryUrl = (u) => u && /^https?:\/\//i.test(u);
|
|
14
|
+
|
|
15
|
+
/* Prefer a normal browser window (not the extension popup) */
|
|
16
|
+
try {
|
|
17
|
+
const normalActive = await chrome.tabs.query({ active: true, windowType: "normal" });
|
|
18
|
+
const na = normalActive.find((x) => tryUrl(x.url));
|
|
19
|
+
if (na?.id != null) return na.id;
|
|
20
|
+
} catch {
|
|
21
|
+
/* windowType not supported in some builds */
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const last = await chrome.windows.getLastFocused({ populate: true });
|
|
25
|
+
if (last.type === "normal" && last.tabs) {
|
|
26
|
+
const t = last.tabs.find((x) => x.active && tryUrl(x.url));
|
|
27
|
+
if (t?.id != null) return t.id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const wins = await chrome.windows.getAll({ populate: true, windowTypes: ["normal"] });
|
|
31
|
+
for (const w of wins) {
|
|
32
|
+
const t = w.tabs?.find((x) => x.active && tryUrl(x.url));
|
|
33
|
+
if (t?.id != null) return t.id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const any = await chrome.tabs.query({});
|
|
37
|
+
const t = any.find((x) => x.active && tryUrl(x.url));
|
|
38
|
+
if (t?.id != null) return t.id;
|
|
39
|
+
|
|
40
|
+
throw new Error("Open an http(s) page in a normal tab and try again.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function ensureInjected(tabId) {
|
|
44
|
+
const [check] = await chrome.scripting.executeScript({
|
|
45
|
+
target: { tabId },
|
|
46
|
+
world: "MAIN",
|
|
47
|
+
func: () => ({
|
|
48
|
+
hasO: typeof window.o !== "undefined",
|
|
49
|
+
hasBridge: typeof window.__objsExtensionBridge !== "undefined",
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
const { hasO, hasBridge } = check.result || {};
|
|
53
|
+
if (hasO && hasBridge) return;
|
|
54
|
+
await chrome.scripting.executeScript({
|
|
55
|
+
target: { tabId },
|
|
56
|
+
world: "MAIN",
|
|
57
|
+
files: ["lib/objs-inject.js", "bridge.js"],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function invokeBridge(tabId, method, arg) {
|
|
62
|
+
await ensureInjected(tabId);
|
|
63
|
+
const [out] = await chrome.scripting.executeScript({
|
|
64
|
+
target: { tabId },
|
|
65
|
+
world: "MAIN",
|
|
66
|
+
func: async (payload) => {
|
|
67
|
+
const b = window.__objsExtensionBridge;
|
|
68
|
+
if (!b) return { error: "Bridge not loaded" };
|
|
69
|
+
try {
|
|
70
|
+
const fn = b[payload.method];
|
|
71
|
+
if (typeof fn !== "function") return { error: "Unknown method: " + payload.method };
|
|
72
|
+
const r = fn.call(b, payload.arg);
|
|
73
|
+
if (r && typeof r.then === "function") {
|
|
74
|
+
return { value: await r };
|
|
75
|
+
}
|
|
76
|
+
return { value: r };
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return { error: String(e && e.message ? e.message : e) };
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
args: [{ method, arg }],
|
|
82
|
+
});
|
|
83
|
+
const res = out.result;
|
|
84
|
+
if (res.error) throw new Error(res.error);
|
|
85
|
+
return res.value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
89
|
+
if (msg.type === "getTabId") {
|
|
90
|
+
getTargetTabId()
|
|
91
|
+
.then((tabId) => sendResponse({ ok: true, tabId }))
|
|
92
|
+
.catch((e) => sendResponse({ ok: false, error: String(e && e.message ? e.message : e) }));
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (msg.type === "invoke") {
|
|
96
|
+
const tabId = msg.tabId;
|
|
97
|
+
if (tabId == null) {
|
|
98
|
+
getTargetTabId()
|
|
99
|
+
.then((id) => invokeBridge(id, msg.method, msg.arg))
|
|
100
|
+
.then((r) => sendResponse({ ok: true, result: r }))
|
|
101
|
+
.catch((e) => sendResponse({ ok: false, error: String(e && e.message ? e.message : e) }));
|
|
102
|
+
} else {
|
|
103
|
+
invokeBridge(tabId, msg.method, msg.arg)
|
|
104
|
+
.then((r) => sendResponse({ ok: true, result: r }))
|
|
105
|
+
.catch((e) => sendResponse({ ok: false, error: String(e && e.message ? e.message : e) }));
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Objs extension bridge — runs in page MAIN world after objs-inject.js
|
|
3
|
+
*/
|
|
4
|
+
(function () {
|
|
5
|
+
function ensureO() {
|
|
6
|
+
if (typeof window.o === "undefined") {
|
|
7
|
+
throw new Error("Objs is not loaded");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Maps extension “Show succeeded” to o.tShowOk (whether OK step lines are written to o.tLog). */
|
|
12
|
+
function applyTShowOkFromPayload(payload) {
|
|
13
|
+
if (payload && payload.overlay && typeof payload.overlay.showSucceeded === "boolean") {
|
|
14
|
+
window.o.tShowOk = payload.overlay.showSucceeded;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Sets o.autotag (data-{name} for selectors); empty string clears it. */
|
|
19
|
+
function applyAutotagFromPayload(payload) {
|
|
20
|
+
if (!payload || typeof payload.autotag !== "string" || typeof window.o === "undefined") return;
|
|
21
|
+
const s = payload.autotag.trim();
|
|
22
|
+
window.o.autotag = s === "" ? undefined : s;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Same as recording example: draggable panel with o.tLog / o.tRes (pass/fail per test). */
|
|
26
|
+
function showTestResultsOverlay() {
|
|
27
|
+
const o = window.o;
|
|
28
|
+
if (typeof o.testOverlay !== "function") return;
|
|
29
|
+
try {
|
|
30
|
+
o.testOverlay();
|
|
31
|
+
if (o.testOverlay.showPanel) o.testOverlay.showPanel();
|
|
32
|
+
} catch {
|
|
33
|
+
/* ignore */
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
window.__objsExtensionBridge = {
|
|
38
|
+
ping() {
|
|
39
|
+
return typeof window.o !== "undefined";
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
/** Open o.testOverlay() + results list on the page (same as after Play). */
|
|
43
|
+
showTestOverlay(payload) {
|
|
44
|
+
ensureO();
|
|
45
|
+
applyTShowOkFromPayload(payload || {});
|
|
46
|
+
showTestResultsOverlay();
|
|
47
|
+
return { shown: true };
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
startRecording(payload) {
|
|
51
|
+
ensureO();
|
|
52
|
+
applyAutotagFromPayload(payload || {});
|
|
53
|
+
const p = payload || {};
|
|
54
|
+
const observe = p.observe ? String(p.observe) : undefined;
|
|
55
|
+
const bag = {};
|
|
56
|
+
if (observe) bag.observe = observe;
|
|
57
|
+
if (p.strictCaptureAssertions) bag.strictCaptureAssertions = true;
|
|
58
|
+
if (p.strictCaptureNetwork) bag.strictCaptureNetwork = true;
|
|
59
|
+
if (p.strictCaptureWebSocket) bag.strictCaptureWebSocket = true;
|
|
60
|
+
if (Object.keys(bag).length > 0) window.o.startRecording(bag);
|
|
61
|
+
else window.o.startRecording();
|
|
62
|
+
return { active: true };
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
stopRecording() {
|
|
66
|
+
ensureO();
|
|
67
|
+
return window.o.stopRecording();
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Stop recording and return both raw recording and o.exportTest() JS (primary format for the extension).
|
|
72
|
+
*/
|
|
73
|
+
stopAndExport(payload) {
|
|
74
|
+
ensureO();
|
|
75
|
+
const recording = window.o.stopRecording();
|
|
76
|
+
const options = Object.assign({}, (payload && payload.options) || {}, { extensionExport: true });
|
|
77
|
+
const script = window.o.exportTest(recording, options);
|
|
78
|
+
return { recording, script };
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run an o.exportTest() script: executes o.test(..., { sync: true }, ...) in-page and polls tFinalized.
|
|
83
|
+
* Legacy scripts with o.addTest are rewritten to o.test + sync (addTest + run() interleaves async steps wrong).
|
|
84
|
+
*/
|
|
85
|
+
runExportedTest(payload) {
|
|
86
|
+
ensureO();
|
|
87
|
+
applyAutotagFromPayload(payload || {});
|
|
88
|
+
applyTShowOkFromPayload(payload);
|
|
89
|
+
const code = String((payload && payload.code) || "");
|
|
90
|
+
if (!code.includes("o.addTest") && !code.includes("o.test")) {
|
|
91
|
+
throw new Error("Script must contain o.test(...) or o.addTest(...) from o.exportTest()");
|
|
92
|
+
}
|
|
93
|
+
let patched = code.trim();
|
|
94
|
+
if (patched.includes("o.addTest")) {
|
|
95
|
+
patched = patched.replace(
|
|
96
|
+
/\bo\.addTest\s*\(\s*(['"])Recorded test\1\s*,\s*\[/,
|
|
97
|
+
"const __objsExtensionTestRun = o.test($1Recorded test$1,"
|
|
98
|
+
);
|
|
99
|
+
patched = patched.replace(
|
|
100
|
+
/\]\s*\n(\s*\/\/ Add manual checks[^\n]*)/,
|
|
101
|
+
"],\n$1"
|
|
102
|
+
);
|
|
103
|
+
patched = patched.replace(
|
|
104
|
+
/\n\s*\]\s*,\s*\(\)\s*=>\s*\{\s*\r?\n\s*\/\/ teardown/,
|
|
105
|
+
",\n{ sync: true }, () => {\n // teardown"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (!patched.includes("__objsExtensionTestRun")) {
|
|
109
|
+
throw new Error("Expected const __objsExtensionTestRun from o.exportTest(); legacy addTest upgrade failed");
|
|
110
|
+
}
|
|
111
|
+
const fullCode = patched + "\nreturn __objsExtensionTestRun;";
|
|
112
|
+
let testId;
|
|
113
|
+
try {
|
|
114
|
+
testId = new Function("o", fullCode)(window.o);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
throw new Error((e && e.message) || String(e));
|
|
117
|
+
}
|
|
118
|
+
const tid = Number(testId);
|
|
119
|
+
if (!Number.isFinite(tid) || tid < 0) {
|
|
120
|
+
throw new Error("o.test did not return a valid test id");
|
|
121
|
+
}
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
try {
|
|
124
|
+
const deadline = Date.now() + 120000;
|
|
125
|
+
let finished = false;
|
|
126
|
+
const poll = () => {
|
|
127
|
+
if (finished) return;
|
|
128
|
+
if (window.o.tFinalized && window.o.tFinalized[tid]) {
|
|
129
|
+
finished = true;
|
|
130
|
+
showTestResultsOverlay();
|
|
131
|
+
resolve({
|
|
132
|
+
testId: tid,
|
|
133
|
+
passed: !!window.o.tRes[tid],
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (Date.now() > deadline) {
|
|
138
|
+
finished = true;
|
|
139
|
+
showTestResultsOverlay();
|
|
140
|
+
resolve({
|
|
141
|
+
testId: tid,
|
|
142
|
+
passed: !!window.o.tRes[tid],
|
|
143
|
+
timedOut: true,
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
setTimeout(poll, 40);
|
|
148
|
+
};
|
|
149
|
+
setTimeout(poll, 0);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
reject(e);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
playRecording(payload) {
|
|
157
|
+
ensureO();
|
|
158
|
+
applyAutotagFromPayload(payload || {});
|
|
159
|
+
applyTShowOkFromPayload(payload);
|
|
160
|
+
const recording = payload.recording;
|
|
161
|
+
const opts = Object.assign({ runAssertions: true }, payload.opts || {});
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
try {
|
|
164
|
+
opts.onComplete = (assertionResult) => {
|
|
165
|
+
showTestResultsOverlay();
|
|
166
|
+
resolve({ assertionResult: assertionResult || null });
|
|
167
|
+
};
|
|
168
|
+
window.o.playRecording(recording, opts);
|
|
169
|
+
} catch (e) {
|
|
170
|
+
reject(e);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
exportTest(payload) {
|
|
176
|
+
ensureO();
|
|
177
|
+
return window.o.exportTest(
|
|
178
|
+
payload.recording,
|
|
179
|
+
Object.assign({}, payload.options || {}, { extensionExport: true }),
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
exportPlaywrightTest(payload) {
|
|
184
|
+
ensureO();
|
|
185
|
+
return window.o.exportPlaywrightTest(payload.recording, payload.options || {});
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
recorderActive() {
|
|
189
|
+
if (typeof window.o === "undefined") return false;
|
|
190
|
+
return !!(window.o.recorder && window.o.recorder.active);
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
})();
|
|
Binary file
|