Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions runtime/auth-broker/tests/helpers/native-fixture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use strict';

const assert = require('node:assert/strict');

// Only for intercepted test pages. A navigation response does not mean that the
// selected document is loaded or that Chrome has painted its native window.
async function settleFixture(connection) {
await connection.command('Page.bringToFront');
const ready = await connection.evaluate(`new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('fixture render timed out')), 5000);
requestAnimationFrame(() => requestAnimationFrame(() => {
clearTimeout(timer);
resolve(document.readyState === 'complete' && document.visibilityState === 'visible');
}));
})`);
assert.equal(ready, true, 'fixture must be loaded and visible before native interaction');
}

async function navigateFixture(connection, url, readyExpression) {
const navigation = await connection.command('Page.navigate', { url });
assert.equal(navigation.errorText, undefined);
assert.ok(navigation.loaderId);
const deadline = Date.now() + 5000;
while (Date.now() < deadline) {
let ready = false;
try {
const { frameTree } = await connection.command('Page.getFrameTree');
ready = frameTree.frame.loaderId === navigation.loaderId
&& await connection.evaluate('document.readyState') === 'complete'
&& await connection.evaluate(readyExpression);
} catch { /* A previous document may be destroyed while navigation commits. */ }
if (ready) return settleFixture(connection);
await new Promise(resolve => setTimeout(resolve, 30));
}
assert.fail('requested fixture document did not load');
}

async function reportNativeFixture(connection) {
if (!connection) return;
try {
const state = await connection.evaluate(`(() => ({
ready: document.readyState, visibility: document.visibilityState,
focused: document.hasFocus(), activeTag: document.activeElement?.tagName,
viewport: [innerWidth, innerHeight, outerWidth, outerHeight],
scroll: [scrollX, scrollY], scale: devicePixelRatio,
fields: Array.from(document.querySelectorAll('input')).map(element => ({
type: element.type, filled: !!element.value, active: element === document.activeElement,
visible: element.offsetParent !== null, rect: element.getBoundingClientRect().toJSON(),
})),
buttons: Array.from(document.querySelectorAll('button')).map(element => ({
type: element.type, visible: element.offsetParent !== null,
rect: element.getBoundingClientRect().toJSON(),
})),
}))()`);
console.error('Native fixture state:', JSON.stringify(state));
} catch { /* Keep the original test failure when the target has closed. */ }
}

module.exports = { navigateFixture, settleFixture, reportNativeFixture };
19 changes: 6 additions & 13 deletions runtime/auth-broker/tests/paycom-assistance-continuation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const path = require('node:path');
const { ChromeBrowserRuntime } = require('../src/browser-runtime');
const { CdpConnection, createTarget } = require('../src/cdp');
const { paycomAdapter, SECURITY_QUESTION_PATH, CLIENT_LANDING_PATH, SNAPSHOT } = require('../../../plugins/paycom/backend/auth/adapter');
const { navigateFixture, settleFixture, reportNativeFixture } = require('./helpers/native-fixture');

test('CAPTCHA continuation stays on the original document and only submits unchanged retained PINs once', { timeout: 60000 }, async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'paycom-assistance-')); fs.chmodSync(root, 0o700);
Expand Down Expand Up @@ -39,19 +40,7 @@ test('CAPTCHA continuation stays on the original document and only submits uncha
.catch(error => { if (!request.url.includes('example.com')) interceptionError = error; });
});
await connection.command('Page.enable'); await connection.command('Fetch.enable', { patterns: [{ urlPattern: '*' }] });
const load = async () => {
const navigation = await connection.command('Page.navigate', { url });
for (let i = 0; i < 100; i++) {
try {
const { frameTree } = await connection.command('Page.getFrameTree');
if (frameTree.frame.loaderId === navigation.loaderId
&& await connection.evaluate('document.readyState') === 'complete'
&& (await connection.evaluate(SNAPSHOT)).captchaPresent) return;
} catch {}
await new Promise(resolve => setTimeout(resolve, 30));
}
assert.fail('fixture page did not load');
};
const load = () => navigateFixture(connection, url, `(${SNAPSHOT}).captchaPresent`);
for (const scenario of ['empty_before', 'still_visible', 'reloaded', 'cleared', 'changed', 'completed']) {
await load();
if (scenario === 'empty_before') await connection.evaluate('document.querySelector("#pin2").value=""; document.querySelector("#pin5").value=""');
Expand All @@ -63,6 +52,7 @@ test('CAPTCHA continuation stays on the original document and only submits uncha
if (scenario !== 'still_visible') await connection.evaluate('document.querySelector("iframe").remove()');
if (scenario === 'cleared') await connection.evaluate('document.querySelector("#pin2").value=""');
if (scenario === 'changed') await connection.evaluate('document.querySelector("#pin2").value="different"');
await settleFixture(connection);
let resumed = 0;
const operation = paycomAdapter.completeBrowserAssistance(browser, context, { loginOnly: true,
resumeAuthentication: async () => { resumed++; return { status: 'authenticated' }; } });
Expand All @@ -78,5 +68,8 @@ test('CAPTCHA continuation stays on the original document and only submits uncha
if (scenario !== 'empty_before') assert.equal(resumed, 0);
if (interceptionError) throw interceptionError;
}
} catch (error) {
await reportNativeFixture(connection);
throw error;
} finally { connection?.close(); await browser?.close(); fs.rmSync(root, { recursive: true, force: true }); }
});
8 changes: 7 additions & 1 deletion runtime/auth-broker/tests/paycom-native-window.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const test = require('node:test');
const { ChromeBrowserRuntime, processGroupAlive } = require('../src/browser-runtime');
const { CdpConnection, createTarget } = require('../src/cdp');
const adapter = require('../../../plugins/paycom/backend/auth/adapter');
const { navigateFixture, reportNativeFixture } = require('./helpers/native-fixture');

test('normal Chrome types exact PINs into locally intercepted forms and preserves the profile', { timeout: 60000 }, async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'dispatch-native-window-'));
Expand Down Expand Up @@ -54,7 +55,9 @@ test('normal Chrome types exact PINs into locally intercepted forms and preserve
pair = indices;
pending = new Promise((resolve, reject) => { resolvePost = resolve; rejectPost = reject; });
pending.catch(() => {});
await connection.command('Page.navigate', { url });
await navigateFixture(connection, url,
`document.querySelector('input[name="firstIndex"]')?.value === ${JSON.stringify(String(pair[0]))}
&& document.querySelector('input[name="secondIndex"]')?.value === ${JSON.stringify(String(pair[1]))}`);
const current = await adapter.waitForState(connection, 5000, new Set(['security_questions_required']));
await adapter.submitNativeChallenge(connection, credentials, current.snapshot.challenge, browser, AbortSignal.timeout(10000));
const form = await pending;
Expand All @@ -81,6 +84,9 @@ test('normal Chrome types exact PINs into locally intercepted forms and preserve
connection = await CdpConnection.connect(target2.webSocketDebuggerUrl);
const cookies = (await connection.command('Network.getCookies', { urls: [url] })).cookies;
assert.equal(cookies.some(cookie => cookie.name === 'fixture_cookie' && cookie.value === 'retained'), true);
} catch (error) {
await reportNativeFixture(connection);
throw error;
} finally {
connection?.close(); await browser?.close(); fs.rmSync(root, { recursive: true, force: true });
}
Expand Down
4 changes: 3 additions & 1 deletion tooling/ci-browser-smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ async function main() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'dispatch-ci-browser-'));
fs.chmodSync(root, 0o700);
let diagnostics = '', browser;
const runtime = new ChromeBrowserRuntime({ stateRoot: path.join(root, 'profiles'), directoryNetwork: false,
// This checks tool availability on a cold hosted runner, not startup latency.
// Production and the real browser tests retain their normal startup limits.
const runtime = new ChromeBrowserRuntime({ stateRoot: path.join(root, 'profiles'), directoryNetwork: false, startTimeoutMs: 60000,
spawnImpl(command, args, options) {
const stdio = [...options.stdio];stdio[2] = 'pipe';
const child = spawn(command, args, { ...options, stdio });
Expand Down
10 changes: 3 additions & 7 deletions tooling/platform-dependencies.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
{
"url": null,
"sha256": null,
"description": "Development CI builds an isolated bundle from the exact Core commit. Before production publication, pin the Core release asset URL and SHA-256 through a reviewed PR.",
"developmentSource": {
"repository": "dispatch-core",
"commit": "90e29d2940eac250b15042e336661562cff72fe1"
}
"url": "https://github.com/dillonlille/dispatch-core/releases/download/v0.0.1/platform-packages.tar.gz",
"sha256": "b233ad949d4bfc931e19ef7e988c3e15e109241e9aa479cbdeb54404ac013990",
"description": "Use the verified platform package bundle published with Core 0.0.1 for DSP checks, builds, and releases. Upgrade the URL and SHA-256 together through a reviewed PR."
}