import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { remoteFixture } from './helpers/w15-project-remote.mjs'; for (const width of [1440, 390]) test(`project remote fixture at ${width}px`, async t => { const { b, mount, wait, errors, network } = await remoteFixture(t, width); const shots = process.env.MOSAIC_UX_SHOTS; if (shots) mkdirSync(shots, { recursive: true }); async function shot(name) { if (!shots) return; await b.screenshot(join(shots, `project-remote-${name}-${width}.png`)); const { contentSize } = await b.call('Page.getLayoutMetrics'); const { data } = await b.call('Page.captureScreenshot', { format: 'png', captureBeyondViewport: true, clip: { x: 0, y: 0, width: contentSize.width, height: contentSize.height, scale: 1 } }); const { writeFile } = await import('node:fs/promises'); await writeFile(join(shots, `project-remote-${name}-${width}-full.png`), Buffer.from(data, 'base64')); } async function input(value) { await b.evaluate(`(()=>{const e=document.querySelector('#project-remote-url');e.value=${JSON.stringify(value)};e.dispatchEvent(new Event('input',{bubbles:true}));return true})()`); } async function submit() { const rect = await b.evaluate(`(()=>{const r=document.querySelector('.project-remote button[type=submit]').getBoundingClientRect();return {x:r.x+r.width/2,y:r.y+r.height/2}})()`); await b.call('Input.dispatchMouseEvent', { type: 'mousePressed', button: 'left', clickCount: 1, ...rect }); await b.call('Input.dispatchMouseEvent', { type: 'mouseReleased', button: 'left', clickCount: 1, ...rect }); } await mount(); await wait(`document.querySelector('#project-remote-url')`); for (const mode of ['light', 'dark']) { await b.evaluate(`(()=>{const e=document.querySelector('#mode');e.value='${mode}';e.dispatchEvent(new Event('change',{bubbles:true}));return true})()`); assert.equal(await b.evaluate(`document.documentElement.dataset.mode`), mode); assert.equal(await b.evaluate(`getComputedStyle(document.querySelector('#project-remote-url')).visibility`), 'visible'); } await b.evaluate(`(()=>{const e=document.querySelector('#mode');e.value='system';e.dispatchEvent(new Event('change',{bubbles:true}));return true})()`); const ax = await b.call('Accessibility.getFullAXTree'); assert.ok(ax.nodes.some(n => n.role?.value === 'textbox' && n.name?.value === 'Existing HTTPS repository URL')); assert.ok(ax.nodes.some(n => n.role?.value === 'button' && n.name?.value === 'Save')); assert.equal(await b.evaluate(`document.querySelector('.project-remote button[type=submit]').getBoundingClientRect().height >= 44`), true); await b.evaluate(`document.querySelector('#project-remote-url').focus(),true`); await b.key('Tab'); assert.equal(await b.evaluate(`document.activeElement.textContent`), 'Save'); await b.key('Tab', 'Tab', 8); assert.equal(await b.evaluate(`document.activeElement.id`), 'project-remote-url'); assert.equal(await b.evaluate('document.documentElement.scrollWidth <= innerWidth'), true); // Halving the CSS viewport models the reflow available at 200% browser zoom. await b.viewport(width / 2, 500); assert.equal(await b.evaluate(`document.querySelector('.project-remote').scrollWidth <= document.querySelector('.project-remote').clientWidth`), true); await b.viewport(width, 1000); await shot('remote-missing'); await b.key('Tab'); await b.key('Tab', 'Tab', 8); await b.key('Escape'); await input('https://git.example/owner/repo.git'); await shot('remote-editing'); await b.evaluate(`window.__mode='hold',true`); await submit(); await wait(`window.__writes.length===1 && !!window.__release`); assert.equal(await b.evaluate(`document.querySelector('.project-remote button[type=submit]').disabled`), true); await b.evaluate(`document.querySelector('.project-remote form').dispatchEvent(new Event('submit',{bubbles:true,cancelable:true})),window.__release(),true`); await wait(`document.querySelector('.project-remote').textContent.includes('Configured, not verified')`); assert.equal(await b.evaluate('window.__writes.length'), 1); await shot('remote-saved-not-published'); await b.evaluate(`window.__remote={project:'stack',remote:null,revision:'${'a'.repeat(64)}',firstSaveAllowed:true};window.__mode='stale';true`); await mount(); await wait(`document.querySelector('#project-remote-url')`); await input('https://git.example/owner/repo.git'); await submit(); await wait(`document.querySelector('.project-remote').textContent.includes('setting changed')`); await shot('remote-stale'); assert.equal(await b.evaluate(`document.querySelector('#project-remote-url').value`), 'https://git.example/owner/repo.git'); await b.evaluate(`document.querySelector('.project-remote button[type=button]').click(),true`); await wait(`document.querySelector('.project-remote').textContent.includes('Review your entry')`); assert.equal(await b.evaluate(`document.querySelector('#project-remote-url').value`), 'https://git.example/owner/repo.git'); await input('https://name:secret@git.example/owner/repo'); await submit(); await wait(`document.querySelector('.project-remote').textContent.includes('must not contain credentials')`); assert.equal(await b.evaluate(`document.querySelector('#project-remote-url').value`), ''); await shot('remote-invalid'); await b.evaluate(`window.__mode='fail',true`); await input('https://git.example/owner/invalid'); await submit(); await wait(`document.querySelector('.project-remote').textContent.includes('Enter a valid HTTPS')`); assert.equal(await b.evaluate(`document.querySelector('#project-remote-url').value`), 'https://git.example/owner/invalid'); await b.evaluate(`window.__mode='unknown',true`); await input('https://git.example/owner/unknown'); await submit(); await wait(`document.querySelector('.project-remote').textContent.includes('Save outcome is unknown')`); assert.equal(await b.evaluate(`document.querySelector('.project-remote button[type=submit]').disabled`), true); assert.equal(await b.evaluate(`document.querySelector('#project-remote-url').value`), 'https://git.example/owner/unknown'); await b.evaluate(`window.__writeEnabled=false,true`); await mount(); await wait(`document.querySelector('.project-remote').textContent.includes('read-only')`); assert.equal(await b.evaluate(`document.querySelectorAll('.project-remote form').length`), 0); await mount(null); await shot('remote-readonly-or-unlinked'); assert.equal(await b.evaluate('document.documentElement.scrollWidth <= innerWidth'), true); await b.evaluate(`document.dispatchEvent(new Event('mosaic-signed-out')),true`); assert.equal(await b.evaluate(`document.querySelector('.project-remote').textContent`), ''); assert.deepEqual(network, []); assert.deepEqual(errors, []); }); for (const width of [1440, 390]) test(`real fixture saves the project remote at ${width}px`, async t => { const { realRemoteFixture } = await import('./helpers/w15-project-remote-real.mjs'); const { browser } = await import('./browser.mjs'); const { readFileSync } = await import('node:fs'); const f = await realRemoteFixture(t), b = await browser({ machine: false }); t.after(() => b.close()); const errors = [], blocked = []; b.on('Runtime.exceptionThrown', p => errors.push(p.exceptionDetails?.text)); b.on('Fetch.requestPaused', p => { if (p.request.url.startsWith(f.base + '/')) b.call('Fetch.continueRequest', { requestId: p.requestId }).catch(() => {}); else { blocked.push(p.request.url); b.call('Fetch.failRequest', { requestId: p.requestId, errorReason: 'BlockedByClient' }).catch(() => {}); } }); await b.call('Fetch.enable', { patterns: [{ urlPattern: '*' }] }); await b.viewport(width, 1000); await b.navigate(f.base + '/#/projects/10000000-0000-4000-8000-000000000001/settings'); await b.evaluate(`new Promise((resolve,reject)=>{const link=document.createElement('link');link.rel='stylesheet';link.href='/project-remote.css';document.head.append(link);const script=document.createElement('script');script.src='/project-remote.js';script.onload=resolve;script.onerror=reject;document.head.append(script)})`); await b.evaluate(`(()=>{const main=document.querySelector('#main');for(const child of main.children)child.style.display='none';main.hidden=false;const h=document.createElement('h1');h.textContent='Project information';const slot=document.createElement('div');main.append(h,slot);MosaicProjectRemote.mount({container:slot,projectId:'10000000-0000-4000-8000-000000000001',repositoryKey:'stack'});return true})()`); const wait = async expression => b.evaluate(`(async()=>{const until=Date.now()+10000;while(!(${expression})){if(Date.now()>until)throw Error('timed out');await new Promise(r=>setTimeout(r,20))}return true})()`); await wait(`document.querySelector('#project-remote-url')`); await b.evaluate(`(()=>{const e=document.querySelector('#project-remote-url');e.value='https://git.example/owner/repo.git';e.dispatchEvent(new Event('input',{bubbles:true}));return true})()`); const rect = await b.evaluate(`(()=>{const r=document.querySelector('.project-remote button[type=submit]').getBoundingClientRect();return {x:r.x+r.width/2,y:r.y+r.height/2}})()`); await b.call('Input.dispatchMouseEvent', { type: 'mousePressed', button: 'left', clickCount: 1, ...rect }); await b.call('Input.dispatchMouseEvent', { type: 'mouseReleased', button: 'left', clickCount: 1, ...rect }); await wait(`document.querySelector('.project-remote').textContent.includes('Configured, not verified')`); assert.equal(JSON.parse(readFileSync(f.file)).projects.stack.remote, 'https://git.example/owner/repo.git'); assert.equal(await b.evaluate('document.documentElement.scrollWidth <= innerWidth'), true); if (process.env.MOSAIC_UX_SHOTS) { mkdirSync(process.env.MOSAIC_UX_SHOTS, { recursive: true }); await b.screenshot(join(process.env.MOSAIC_UX_SHOTS, `project-remote-real-saved-${width}.png`)); const { contentSize } = await b.call('Page.getLayoutMetrics'); const { data } = await b.call('Page.captureScreenshot', { format: 'png', captureBeyondViewport: true, clip: { x: 0, y: 0, width: contentSize.width, height: contentSize.height, scale: 1 } }); const { writeFile } = await import('node:fs/promises'); await writeFile(join(process.env.MOSAIC_UX_SHOTS, `project-remote-real-saved-${width}-full.png`), Buffer.from(data, 'base64')); } assert.deepEqual(blocked, []); assert.deepEqual(errors, []); });