Ctrl K
ComunidadGuías…

test/contextual-clay-geometry-browser.test.cjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
var test = require('node:test');
var assert = require('node:assert/strict');
var cp = require('node:child_process');
var path = require('node:path');
var fs = require('node:fs');
var playwright = require('playwright');

async function waitForSettledReviewPaint(target) {
  await target.evaluate(async function () {
    // Flush theme styles before enumerating finite CSS transitions.
    document.querySelectorAll('.ask-user-option').forEach(function (button) { getComputedStyle(button).backgroundColor; });
    await new Promise(function (resolve) { requestAnimationFrame(resolve); });
    var animations = document.getAnimations().filter(function (animation) { return animation.effect && animation.effect.getComputedTiming().iterations !== Infinity; });
    await Promise.all(animations.map(function (animation) { return animation.finished.catch(function () {}); }));
    await new Promise(function (resolve) { requestAnimationFrame(function () { requestAnimationFrame(resolve); }); });
  });
}
async function reviewPaint(target) {
  return target.evaluate(function () {
    function luminance(color) {
      var channels = color.match(/[\d.]+/g).slice(0, 3).map(function (value) { var channel = Number(value) / 255; return channel <= .04045 ? channel / 12.92 : Math.pow((channel + .055) / 1.055, 2.4); });
      return channels[0] * .2126 + channels[1] * .7152 + channels[2] * .0722;
    }
    var probe = document.createElement('span'); probe.style.backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--bg-alt');
    return { light: document.documentElement.classList.contains('light-theme'), expectedBackground: probe.style.backgroundColor, buttons: Array.from(document.querySelectorAll('.ask-user-container:not(.answered) .ask-user-option')).map(function (button) {
      var background = getComputedStyle(button).backgroundColor;
      var color = getComputedStyle(button.querySelector('.option-label')).color;
      var a = luminance(background); var b = luminance(color);
      return { text: button.textContent.trim(), background: background, color: color, contrast: (Math.max(a, b) + .05) / (Math.min(a, b) + .05) };
    }) };
  });
}
function assertSettledReview(paint, light) {
  assert.equal(paint.light, light); assert.equal(paint.buttons.length, 2);
  paint.buttons.forEach(function (button) { assert.equal(button.background, paint.expectedBackground, 'Settled option uses the active theme background'); assert.ok(button.contrast >= 4.5, button.text + ' contrast: ' + button.contrast); });
}

test('production popover geometry: drag, resize, continuity, cancellation, keyboard and owner persistence', 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 (v) { output += v; }); child.stderr.on('data', function (v) { output += v; });
  var info = await new Promise(function (resolve, reject) { var timer = setTimeout(function () { reject(new Error(output)); }, 15000); child.once('message', function (value) { clearTimeout(timer); resolve(value); }); });
  var browser = await playwright.chromium.launch(); t.after(async function () { await browser.close(); });
  var page = await browser.newPage({ viewport: { width: 1280, height: 900 }, reducedMotion: 'reduce' }); page.setDefaultTimeout(10000);
  var errors = []; page.on('pageerror', function (error) { errors.push(error.message); });
  var origin = 'http://127.0.0.1:' + info.port;
  await page.goto(origin + '/fixture/login?to=alpha');
  var trigger = page.locator('.icon-strip-brand'); await page.waitForFunction(function () { return !document.querySelector('.icon-strip-brand').disabled; });
  await trigger.click();
  var shell = page.locator('#contextual-clay'); var header = shell.locator('header');
  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'); var draft = 'Help me organise this project. Keep my current session open while we review the next steps.'; await input.fill(draft);
  var context = page.frames().find(function (item) { return item.url().includes('contextual=1'); });
  var session = await context.evaluate(async function () { window.geometryMarker = 'same renderer'; window.geometryStore = (await import('./modules/store.js')).store; return window.geometryStore.get('paneSessionId'); });
  // Real wheel input disarms the native history-replay sticky-bottom window.
  await frame.locator('#messages').hover(); await page.mouse.wheel(0, -500);
  await context.evaluate(async function () { window.geometryScroll = await import('./modules/chat-render-runtime.js'); });
  await context.waitForFunction(function () { return !window.geometryScroll.getStickyBottom(); });
  var scrollPosition = await context.evaluate(function () { var scroll = document.getElementById('messages'); scroll.scrollTop = 45; document.getElementById('input').setSelectionRange(12, 20); return scroll.scrollTop; });
  assert.ok(scrollPosition > 0, 'Real transcript is scrolled away from the latest message');
  await page.evaluate(async function () { window.geometryState = (await import('/p/alpha/modules/store.js')).store; });
  var initial = await shell.boundingBox(); var heading = await header.boundingBox();
  var saves = 0; page.on('request', function (request) { if (request.method() === 'PUT' && request.url().endsWith('/api/user/contextual-clay-geometry')) saves++; });
  await page.mouse.move(heading.x + 45, heading.y + 16); await page.mouse.down();
  await page.mouse.move(heading.x + 210, heading.y + 85, { steps: 8 });
  assert.equal(saves, 0, 'Do not write during pointer movement');
  await page.mouse.up();
  var moved = await shell.boundingBox(); assert.ok(moved.x > initial.x + 140); assert.ok(moved.y > initial.y + 50);
  assert.equal(await context.evaluate(function () { return document.getElementById('messages').scrollTop; }), scrollPosition);
  assert.deepEqual(await input.evaluate(function (el) { return [el.selectionStart, el.selectionEnd]; }), [12, 20]);
  await page.waitForFunction(function () { var value = window.geometryState.get('contextualClayGeometry'); return !value.saving && !value.pending; });
  assert.equal(saves, 1);
  var corner = await shell.locator('[data-clay-resize="se"]').boundingBox();
  await page.mouse.move(corner.x + 9, corner.y + 9); await page.mouse.down(); await page.mouse.move(corner.x + 125, corner.y + 89, { steps: 8 }); await page.mouse.up();
  var resized = await shell.boundingBox(); assert.ok(resized.width > moved.width + 100); assert.ok(resized.height > moved.height + 60);
  assert.ok(await context.evaluate(function () { var el = document.getElementById('messages'); return el.scrollTop < el.scrollHeight - el.clientHeight - 150; }), 'Resizing retains the reader position rather than forcing latest');
  assert.equal(await input.inputValue(), draft); assert.equal(await context.evaluate(function () { return window.geometryMarker; }), 'same renderer');
  assert.equal(await context.evaluate(function () { return window.geometryStore.get('paneSessionId'); }), session);
  var review = page.request.get(origin + '/fixture/propose?viewer=alpha&target=beta');
  var card = frame.locator('.ask-user-container:not(.answered)'); await card.waitFor(); var reviewId = await card.getAttribute('data-tool-id');
  await header.focus(); await page.keyboard.press('ArrowRight'); await shell.locator('[data-clay-resize="se"]').focus(); await page.keyboard.press('ArrowLeft');
  assert.equal(await card.getAttribute('data-tool-id'), reviewId); assert.equal(await input.inputValue(), draft);
  await page.request.get(origin + '/fixture/stream'); await frame.locator('#generation-stop').waitFor({ state: 'visible' });
  var beforeCancel = await shell.boundingBox(); heading = await header.boundingBox();
  await page.mouse.move(heading.x + 45, heading.y + 16); await page.mouse.down(); await page.mouse.move(900, 500); await page.keyboard.press('Escape'); await page.mouse.up();
  var cancelled = await shell.boundingBox(); assert.ok(Math.abs(cancelled.x - beforeCancel.x) < 2); assert.equal(await shell.isVisible(), true);
  assert.equal(await shell.evaluate(function (el) { return el.classList.contains('contextual-clay-manipulating'); }), false);
  heading = await header.boundingBox(); await page.mouse.move(heading.x + 45, heading.y + 16); await page.mouse.down(); await page.mouse.move(heading.x + 70, heading.y + 30);
  await header.dispatchEvent('pointercancel', { pointerId: 1 }); await page.mouse.up();
  assert.equal(await shell.evaluate(function (el) { return el.classList.contains('contextual-clay-manipulating'); }), false);
  assert.ok(Math.abs((await shell.boundingBox()).x - beforeCancel.x) < 2);
  var handle = shell.locator('[data-clay-resize="se"]'); corner = await handle.boundingBox();
  await page.mouse.move(corner.x + 9, corner.y + 9); await page.mouse.down(); await page.mouse.move(0, 0); await page.mouse.up();
  var minimum = await shell.boundingBox(); assert.equal(Math.round(minimum.width), 340); assert.equal(Math.round(minimum.height), 360);
  corner = await handle.boundingBox(); await page.mouse.move(corner.x + 9, corner.y + 9); await page.mouse.down(); await page.mouse.move(3000, 2500); await page.mouse.up();
  var maximum = await shell.boundingBox(); assert.ok(maximum.x + maximum.width <= 1273); assert.ok(maximum.y + maximum.height <= 893);
  // Choose a readable final rectangle and wait for the serialized final save.
  await page.evaluate(async function () { window.geometryState = (await import('/p/alpha/modules/store.js')).store; });
  await page.waitForFunction(function () { var value = window.geometryState.get('contextualClayGeometry'); return !value.saving && !value.pending; });
  var saved = (await page.request.get(origin + '/api/user/contextual-clay-geometry')).json(); saved = (await saved).geometry;
  var foreign = await browser.newContext();
  var foreignResponse = await foreign.request.get(origin + '/api/user/contextual-clay-geometry', { headers: { Cookie: 'relay_auth_user=fixture-foreign-token' } });
  assert.equal(foreignResponse.status(), 200); assert.equal((await foreignResponse.json()).geometry, null);
  assert.equal((await foreign.request.put(origin + '/api/user/contextual-clay-geometry', { headers: { Cookie: 'relay_auth_user=fixture-foreign-token', 'X-Clay-Settings': '1' }, data: { left: 20, top: 20, width: 400, height: 500 } })).status(), 200);
  assert.deepEqual((await (await page.request.get(origin + '/api/user/contextual-clay-geometry')).json()).geometry, saved);
  assert.equal((await foreign.request.put(origin + '/api/user/contextual-clay-geometry', { headers: { Cookie: 'relay_auth_user=fixture-foreign-token', 'X-Clay-Settings': '1' }, data: { left: -1, top: 0, width: 400, height: 500 } })).status(), 400);
  assert.equal((await foreign.request.get(origin + '/api/user/contextual-clay-geometry')).status(), 401);
  await foreign.close();
  await page.getByRole('button', { name: 'Close Clay conversation', exact: true }).click(); await trigger.click();
  assert.equal(await input.inputValue(), draft); assert.equal(await card.getAttribute('data-tool-id'), reviewId);
  var reopen = await shell.boundingBox(); assert.ok(Math.abs(reopen.width - maximum.width) < 2);
  await page.setViewportSize({ width: 390, height: 844 }); var mobile = await shell.boundingBox(); assert.ok(mobile.x >= 0 && mobile.x + mobile.width <= 390); assert.equal(await handle.isVisible(), false);
  await frame.locator('#contextual-attach-btn').click();
  assert.equal(await frame.locator('#attach-image-btn').isVisible(), true);
  await frame.locator('#attach-image-btn').press('Escape');
  await page.setViewportSize({ width: 800, height: 600 });
  var clamped = await shell.boundingBox(); assert.ok(clamped.width <= 784 && clamped.height <= 584);
  var savesBeforeClick = saves; heading = await header.boundingBox(); await page.mouse.click(heading.x + 45, heading.y + 16); assert.equal(saves, savesBeforeClick);
  await page.setViewportSize({ width: 1280, height: 900 });
  await page.waitForFunction(function (width) { return Math.abs(document.getElementById('contextual-clay').getBoundingClientRect().width - width) < 2; }, maximum.width);
  var restored = await shell.boundingBox(); assert.ok(Math.abs(restored.width - maximum.width) < 2);
  assert.deepEqual((await (await page.request.get(origin + '/api/user/contextual-clay-geometry')).json()).geometry, saved);
  var beforeHeaderClick = saves; heading = await header.boundingBox(); await page.mouse.click(heading.x + 45, heading.y + 16); assert.equal(saves, beforeHeaderClick);
  assert.equal(await frame.locator('#generation-stop').isVisible(), true);
  await page.screenshot({ path: '/tmp/contextual-geometry-desktop.png' });
  await page.evaluate(async function () { (await import('/p/alpha/modules/theme.js')).applyTheme('clay-dark'); });
  await context.evaluate(async function () { (await import('./modules/theme.js')).applyTheme('clay-dark'); });
  await waitForSettledReviewPaint(page); await waitForSettledReviewPaint(context);
  var darkPaint = await reviewPaint(context); assertSettledReview(darkPaint, false);
  await page.screenshot({ path: '/tmp/contextual-geometry-desktop-dark.png' });
  await page.setViewportSize({ width: 390, height: 844 }); await page.screenshot({ path: '/tmp/contextual-geometry-mobile.png' });
  await page.evaluate(async function () { (await import('/p/alpha/modules/theme.js')).applyTheme('clay-light'); });
  await context.evaluate(async function () { (await import('./modules/theme.js')).applyTheme('clay-light'); });
  var immediateLightPaint = await reviewPaint(context);
  await waitForSettledReviewPaint(page); await waitForSettledReviewPaint(context);
  var settledLightPaint = await reviewPaint(context); assertSettledReview(settledLightPaint, true);
  t.diagnostic('Dark-to-light review paint: ' + JSON.stringify({ immediate: immediateLightPaint, settled: settledLightPaint }));
  await page.screenshot({ path: '/tmp/contextual-review-theme-settled-light.png' });
  var capturedLightPaint = await reviewPaint(context); assertSettledReview(capturedLightPaint, true);
  fs.writeFileSync('/tmp/contextual-review-theme-paint.json', JSON.stringify({ capturedAt: new Date().toISOString(), cause: 'The option background has a 150 ms CSS transition; label colour changes immediately.', dark: darkPaint, immediateLight: immediateLightPaint, settledLight: settledLightPaint, afterScreenshot: capturedLightPaint, screenshot: '/tmp/contextual-review-theme-settled-light.png' }, null, 2));
  await page.screenshot({ path: '/tmp/contextual-geometry-mobile-light.png' });
  await page.setViewportSize({ width: 1280, height: 900 });
  await card.getByText('Keep unchanged', { exact: true }).click(); await card.locator('.ask-user-submit').click(); assert.equal((await (await review).json()).status, 'cancelled');
  await page.reload(); await page.waitForFunction(function () { return !document.querySelector('.icon-strip-brand').disabled; }); await trigger.click();
  await page.evaluate(async function () { window.geometryState = (await import('/p/alpha/modules/store.js')).store; });
  await page.waitForFunction(function () { return !!window.geometryState.get('contextualClayGeometry').desired; });
  var reloaded = await shell.boundingBox(); assert.ok(Math.abs(reloaded.width - saved.width) < 2); assert.ok(Math.abs(reloaded.height - saved.height) < 2);
  await shell.getByLabel('What this Mate can see', { exact: true }).click(); assert.equal(await shell.locator('.contextual-clay-context').getAttribute('open'), ''); await page.keyboard.press('Escape'); assert.equal(await shell.isVisible(), true);
  await frame.locator('#messages').getByText('Isolated canonical integration conversation.', { exact: true }).waitFor();
  await frame.locator('#input-wrapper:not([inert])').waitFor();
  await page.getByRole('button', { name: 'Open Clay', exact: true }).click(); await page.waitForURL('**/p/' + info.sourceSlug + '/');
  assert.deepEqual(errors, []);
  t.diagnostic('Observed real pointer drag/resize, 340×360 minimum, viewport maximum, Escape cancellation, keyboard controls, persisted reload, mobile restoration, and unchanged draft/session/review/Stop.');
});