test/contextual-clay-workspace.test.cjs
Rama main · réplica de lectura del repositorio original.
var test = require('node:test');
var assert = require('node:assert/strict');
var cp = require('node:child_process');
var path = require('node:path');
var playwright = require('playwright');
test('Open Clay enters the native Mate route with one writable draft and exact review/session; Back restores project', async function (t) {
var child = cp.fork(path.join(__dirname, 'fixtures/contextual-clay-server.cjs'), [], { stdio: ['ignore', 'pipe', 'pipe', 'ipc'] });
t.after(function () { child.kill('SIGTERM'); });
var output = '';
child.stdout.on('data', function (value) { output += value; }); child.stderr.on('data', function (value) { output += value; });
var info = await new Promise(function (resolve, reject) {
var timer = setTimeout(function () { reject(new Error(output)); }, 15000);
child.once('message', function (info) { clearTimeout(timer); resolve(info); });
});
var browser = await playwright.chromium.launch();
t.after(async function () { await browser.close(); });
var page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
page.setDefaultTimeout(8000);
page.on('pageerror', function (error) { t.diagnostic('Browser error: ' + error.stack); });
var origin = 'http://127.0.0.1:' + info.port;
await page.goto(origin + '/fixture/login?to=alpha');
await page.waitForFunction(function () { return !document.querySelector('.icon-strip-brand').disabled; });
var projectSession = await page.evaluate(async function () { return (await import('/p/alpha/modules/store.js')).store.get('activeSessionId'); });
await page.locator('#input').fill('Original project draft');
var trigger = page.locator('.icon-strip-brand');
await trigger.click();
assert.equal(await page.locator('#contextual-clay [data-clay-expand] span').textContent(), 'Open Clay');
var frame = page.frameLocator('#contextual-clay iframe');
await frame.locator('#messages').getByText('Isolated canonical integration conversation.', { exact: true }).waitFor();
await frame.locator('#input-wrapper:not([inert])').waitFor();
var input = frame.locator('#input');
await input.fill('Help me organise this project. Keep my current session open while we review the next steps.');
var frameWindow = page.frames().find(function (item) { return item.url().includes('contextual=1'); });
await frameWindow.evaluate(async function () {
window.rendererIdentity = 'preserved'; window.sent = [];
var ws = (await import('./modules/ws-ref.js')).getWs(); var send = ws.send.bind(ws);
ws.send = function (value) { window.sent.push(JSON.parse(value)); return send(value); };
});
for (var width of [1280, 390]) {
await page.setViewportSize({ width: width, height: 844 });
await frame.locator('#contextual-attach-btn').click();
for (var selector of ['#attach-file-btn', '#attach-image-btn']) {
assert.equal(await frame.locator(selector).isVisible(), true, selector + ' is available through + at ' + width);
assert.ok((await frame.locator(selector).boundingBox()).width >= 36);
}
await frame.locator('#attach-file-btn').press('Escape');
}
await frame.locator('#contextual-attach-btn').click();
var fileChooser = page.waitForEvent('filechooser');
await frame.locator('#attach-file-btn').click();
await (await fileChooser).setFiles({ name: 'handoff.txt', mimeType: 'text/plain', buffer: Buffer.from('Isolated attachment continuity check.') });
await frame.locator('#image-preview-bar').getByText('handoff.txt', { exact: false }).waitFor();
await frame.locator('#contextual-attach-btn').click();
var imageChooser = page.waitForEvent('filechooser');
await frame.locator('#attach-image-btn').click();
await (await imageChooser).setFiles({ name: 'pixel.png', mimeType: 'image/png', buffer: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN1sAAAAASUVORK5CYII=', 'base64') });
await frame.locator('#image-preview-bar img').waitFor();
var selectedViewer = await browser.newPage();
await selectedViewer.goto(origin + '/fixture/login?to=alpha');
await selectedViewer.goto(origin + '/p/beta/');
await selectedViewer.locator('#messages').getByText('BETA_RENDERED_DESTINATION', { exact: true }).waitFor();
await page.bringToFront();
var review = page.request.get(origin + '/fixture/propose?viewer=beta&target=alpha');
await frame.locator('.ask-user-container:not(.answered)').waitFor();
var reviewId = await frame.locator('.ask-user-container:not(.answered)').getAttribute('data-tool-id');
await page.getByRole('button', { name: 'Open Clay', exact: true }).click();
await page.waitForURL('**/p/' + info.sourceSlug + '/');
await page.waitForFunction(function (session) { return document.body.classList.contains('contextual-clay-native') && document.getElementById('input').value.includes('Help me organise') && !document.getElementById('input-wrapper').inert; }, info.sourceSessionId);
assert.equal(await page.evaluate(async function () { var state = (await import('/p/alpha/modules/store.js')).store; return state.get('currentSlug'); }), info.sourceSlug);
assert.equal(await page.evaluate(async function () { return (await import('/p/alpha/modules/store.js')).store.get('activeSessionId'); }), info.sourceSessionId);
assert.equal(await page.locator('body').evaluate(function (body) { return body.classList.contains('mate-workspace-active'); }), true);
assert.equal(await page.locator('#contextual-clay').isVisible(), false, 'No expanded iframe overlay');
assert.equal(await input.inputValue(), '', 'The hidden popover does not keep a second draft');
assert.equal(await input.evaluate(function (input) { return input.readOnly; }), true);
assert.match(await page.locator('#image-preview-bar').textContent(), /handoff.txt/);
assert.equal(await page.locator('#image-preview-bar img').count(), 1);
await page.evaluate(async function () {
window.nativeSent = []; var ws = (await import('/p/alpha/modules/ws-ref.js')).getWs(); var send = ws.send.bind(ws);
ws.send = function (value) { window.nativeSent.push(JSON.parse(value)); return send(value); };
});
var nativeReview = page.locator('.ask-user-container[data-tool-id="' + reviewId + '"]');
await nativeReview.getByText('Keep unchanged', { exact: true }).click();
await nativeReview.locator('.ask-user-submit').click();
assert.equal((await (await review).json()).status, 'cancelled');
assert.equal(await page.evaluate(function () { return window.nativeSent.filter(function (event) { return event.type === 'ask_user_response'; }).length; }), 1);
assert.equal(await frameWindow.evaluate(function () { return window.sent.filter(function (event) { return event.type === 'ask_user_response'; }).length; }), 0);
await page.evaluate(async function () { var store = (await import('/p/alpha/modules/store.js')).store; window.nativeStore = store; window.nativeWsModule = await import('/p/alpha/modules/ws-ref.js'); window.handoffs = []; store.subscribe(function (value, prev) { if (value.contextualWorkspace !== prev.contextualWorkspace) window.handoffs.push({ stage: value.contextualWorkspace && value.contextualWorkspace.stage, images: value.contextualWorkspace && value.contextualWorkspace.draft && value.contextualWorkspace.draft.images.length }); }); });
await page.bringToFront();
await page.evaluate(async function () { window.oldNativeSocket = (await import('/p/alpha/modules/ws-ref.js')).getWs(); window.oldNativeSocket.close(); });
await page.waitForFunction(function () { return window.oldNativeSocket.readyState === 3 && window.handoffs.some(function (handoff) { return handoff.stage === 'opening'; }); });
await page.waitForFunction(function () { return window.nativeStore.get('connected') && window.nativeStore.get('contextualWorkspace').stage === 'workspace' && !window.nativeStore.get('replayingHistory') && window.nativeWsModule.getWs() !== window.oldNativeSocket && document.getElementById('input').value.includes('Help me organise') && document.querySelectorAll('#image-preview-bar img').length === 1 && !document.getElementById('input-wrapper').inert; });
t.diagnostic('Reconnect state: ' + JSON.stringify(await page.evaluate(async function () { var store = (await import('/p/alpha/modules/store.js')).store; return { stage: store.get('contextualWorkspace').stage, connected: store.get('connected'), hidden: document.hidden, images: document.querySelectorAll('#image-preview-bar img').length, native: document.body.className, input: document.getElementById('input').value }; })));
t.diagnostic('Reconnect handoffs: ' + JSON.stringify(await page.evaluate(function () { return window.handoffs; })));
assert.equal(await page.locator('#image-preview-bar img').count(), 1, 'Reconnect preserves attachment ownership');
assert.match(await page.locator('#image-preview-bar').textContent(), /handoff.txt/);
await page.evaluate(async function () {
window.nativeStop = []; var ws = (await import('/p/alpha/modules/ws-ref.js')).getWs(); var send = ws.send.bind(ws);
ws.send = function (value) { window.nativeStop.push(JSON.parse(value)); return send(value); };
});
var nativeDraft = 'Edited in the native Clay workspace. Help me organise this project. Keep my current session open while we review the next steps.';
await page.locator('#input').fill(nativeDraft);
await page.request.get(origin + '/fixture/stream');
await page.locator('#messages').getByText('CONTEXTUAL_STREAM_CONTINUES', { exact: false }).waitFor();
for (var width of [1280, 390]) {
await page.setViewportSize({ width: width, height: 844 });
await page.waitForFunction(function () { var input = document.getElementById('input'); return input.scrollHeight <= input.clientHeight + 1 || getComputedStyle(input).overflowY === 'auto'; });
var stop = await page.locator('#generation-stop').boundingBox();
var mic = await page.locator('#stt-btn').boundingBox();
assert.ok(stop.x + stop.width <= mic.x, 'Native Stop has a separate slot at ' + width);
assert.equal(await page.locator('#input').inputValue(), nativeDraft);
await page.screenshot({ path: '/tmp/contextual-native-mate-' + width + '.png', animations: 'disabled' });
await page.evaluate(async function () { (await import('/p/alpha/modules/theme.js')).applyTheme('clay-dark'); });
await page.screenshot({ path: '/tmp/contextual-native-mate-dark-' + width + '.png', animations: 'disabled' });
await page.evaluate(async function () { (await import('/p/alpha/modules/theme.js')).applyTheme('clay-light'); });
}
await page.locator('#generation-stop').click();
assert.equal(await page.evaluate(function () { return window.nativeStop.filter(function (event) { return event.type === 'stop'; }).length; }), 1);
assert.equal(await frameWindow.evaluate(function () { return window.sent.filter(function (event) { return event.type === 'stop'; }).length; }), 0);
await page.getByRole('button', { name: 'Back to project', exact: true }).click();
await page.waitForURL('**/p/alpha/');
await page.waitForFunction(function () { return document.getElementById('input').value === 'Original project draft' && !document.getElementById('input-wrapper').inert; });
assert.equal(await page.evaluate(async function () { return (await import('/p/alpha/modules/store.js')).store.get('activeSessionId'); }), projectSession);
await trigger.click();
assert.equal(await input.inputValue(), nativeDraft);
assert.equal(await frameWindow.evaluate(function () { return window.rendererIdentity; }), 'preserved');
assert.equal(await input.evaluate(function (input) { return input.readOnly; }), false);
assert.equal(await frame.locator('#image-preview-bar img').count(), 1);
assert.match(await frame.locator('#image-preview-bar').textContent(), /handoff.txt/);
await frame.locator('#messages').getByText('CONTEXTUAL_STREAM_CONTINUES', { exact: false }).waitFor();
assert.equal(await frame.locator('.ask-user-container[data-tool-id="' + reviewId + '"]').getAttribute('class').then(function (value) { return value.includes('answered'); }), true);
await trigger.click();
await page.waitForURL('**/p/' + info.sourceSlug + '/');
await page.locator('#contextual-workspace-back').waitFor();
await trigger.click();
await page.waitForURL('**/p/alpha/');
await trigger.click();
await page.request.get(origin + '/fixture/remove-contextual-session');
await frameWindow.evaluate(async function () { (await import('./modules/ws-ref.js')).getWs().close(); });
await frame.locator('#contextual-session-warning').waitFor();
await page.getByRole('button', { name: 'Open Clay', exact: true }).click();
assert.equal(page.url(), origin + '/p/alpha/', 'A removed exact conversation cannot silently open a different native session');
assert.equal(await input.inputValue(), nativeDraft);
t.diagnostic('Actual route/native Mate shell, exact session, review decline, stream, draft/image/file transfer, direct file chooser handlers at both widths, Back and trigger cycle passed.');
});