feat(marketplace): match CodeGraph theme with local Lucide assets [skip ci]

This commit is contained in:
Forge
2026-09-23 04:50:13 +00:00
parent 7bbce09f4b
commit b4dcb40d92
26 changed files with 1050 additions and 37 deletions
@@ -22,7 +22,7 @@ let server,bridge,browser,page;
bridge=await startExtensionBridge([root],origin);browser=await chromium.launch({executablePath:process.env.CHROME_PATH||'/opt/google/chrome/chrome',headless:true,args:['--no-sandbox']});page=await browser.newPage({viewport:{width:1440,height:1000},acceptDownloads:true});page.setDefaultTimeout(30000);const errors=[];page.on('pageerror',e=>errors.push(e.message));
await page.goto(origin+'/extensions/source-example');await page.locator('[data-view-source]').click();await page.locator('#source-content').waitFor();assert.match(await page.locator('.source-verified').innerText(),/9.0.0/);assert.equal(await page.evaluate(()=>state.snapshot),null);assert.deepEqual(routes(),[]);assert.equal(await page.locator('#source-file option').count(),5);pass('unconnected browser lists every runtime dependency and manifest before any install');
await page.locator('#source-file').selectOption('readme.md');assert.equal(await page.locator('#source-content').innerText(),JSON.parse(artifacts[1]).files['readme.md']);assert.equal(await page.locator('#source-content script, #source-content img').count(),0);assert.equal(await page.evaluate(()=>window.__packageExecuted),undefined);pass('HTML and executable extension source render inertly as literal text');
await page.locator('#source-file').selectOption('large.txt');assert.equal((await page.locator('#source-content').innerText()).length,64000);await page.locator('#source-next').click();assert.match(await page.locator('#source-range').innerText(),/64001/);pass('large source content is paged without truncating downloadable files');
await page.locator('#source-file').selectOption('large.txt');assert.equal((await page.locator('#source-content').innerText()).length,64000);await page.locator('#source-next svg > *').first().dispatchEvent('click');assert.match(await page.locator('#source-range').innerText(),/64001/);await page.locator('#source-prev svg > *').first().dispatchEvent('click');assert.match(await page.locator('#source-range').innerText(),/Characters 1–64000/);pass('large source content is paged by nested SVG clicks without truncating downloadable files');
const dl=page.waitForEvent('download');await page.locator('#source-package-download').click();const downloaded=await dl;await downloaded.saveAs(path.join(out,'verified-download.cgext'));assert.equal(hash(fs.readFileSync(path.join(out,'verified-download.cgext'))),hash(artifacts[1]));pass('source download uses the same verified immutable bytes');
assert.match(await page.locator('.source-inspection').innerText(),/Operator checked.*public committed package/s);await page.locator('#source-file').selectOption('index.cjs');await page.screenshot({path:path.join(out,'desktop-source.png'),fullPage:true});
// Connect after inspecting incompatible catalog 9.0.0. The engine chooses 1.0.0.
@@ -37,7 +37,7 @@ let server,bridge,browser,page;
await page.locator('[data-view-source]').click();await page.locator('.source-verified').filter({hasText:'2.0.0'}).waitFor();await page.getByRole('button',{name:'Install extension'}).click();await page.getByRole('button',{name:'Disable',exact:true}).waitFor();assert.deepEqual(routes(),['/inspected-2.0.0']);pass('reviewed exact version installs once and produces actual SQLite graph');
await page.setViewportSize({width:390,height:844});await page.screenshot({path:path.join(out,'mobile-source.png'),fullPage:true});assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));pass('mobile file inspection fits viewport and preserves literal source');
await publish(artifacts[3]);await page.evaluate(()=>refresh());await page.locator('.compatibility').filter({hasText:'Selected 3.0.0'}).waitFor();await page.getByRole('button',{name:'Update to 3.0.0'}).click();assert.deepEqual(routes(),['/inspected-2.0.0']);await page.locator('[data-view-source]').click();await page.locator('.source-verified').filter({hasText:'3.0.0'}).waitFor();await page.getByRole('button',{name:'Update to 3.0.0'}).click();await page.waitForFunction(()=>state.snapshot?.error&&!state.snapshot.busy);assert.deepEqual(routes(),['/inspected-2.0.0']);pass('review required for changed update; actual throwing update preserves working graph');
await page.getByRole('button',{name:'Disable',exact:true}).click();await page.getByRole('button',{name:'Enable',exact:true}).waitFor();assert.deepEqual(routes(),[]);await page.getByRole('button',{name:'Remove',exact:true}).click();await page.getByText('Extension removed and graph refreshed',{exact:true}).first().waitFor();assert.deepEqual(routes(),[]);pass('disable and removal retain managed lifecycle behavior');
await page.getByRole('button',{name:'Disable',exact:true}).locator('svg > *').first().dispatchEvent('click');await page.getByRole('button',{name:'Enable',exact:true}).waitFor();assert.deepEqual(routes(),[]);await page.getByRole('button',{name:'Remove',exact:true}).locator('svg > *').first().dispatchEvent('click');await page.getByText('Extension removed and graph refreshed',{exact:true}).first().waitFor();assert.deepEqual(routes(),[]);pass('disable and removal retain managed lifecycle behavior');
// Exercise malformed package identity/path/display bounds using the real browser verifier, no package execution.
const negative=await page.evaluate(async()=>{const results=[];for(const kind of ['identity','path','binary','files','size']){const p={format:'codegraph-extension-1',package:{version:'1.0.0',main:'index.cjs',codegraph:{id:'check'}},files:{'index.cjs':'module.exports={}'}};if(kind==='identity')p.package.version='2.0.0';if(kind==='path')p.files['../escape.cjs']='x';if(kind==='binary')p.files['index.cjs']='\0';if(kind==='files')for(let i=0;i<257;i++)p.files[i+'.txt']='x';const bytes=new TextEncoder().encode(JSON.stringify(p)),integrity=[...new Uint8Array(await crypto.subtle.digest('SHA-256',bytes))].map(b=>b.toString(16).padStart(2,'0')).join('');try{await verifiedSource(new Response(bytes,{headers:kind==='size'?{'content-length':String(8*1024*1024+1)}:{}}),{id:'check',version:'1.0.0',integrity});results.push(false);}catch{results.push(true);}}return results;});assert.deepEqual(negative,[true,true,true,true,true]);pass('browser verifier rejects identity traversal NUL excessive files and size violations');
assert.deepEqual(errors,[]);fs.writeFileSync(path.join(out,'summary.json'),JSON.stringify({success:true,checks,browser:browser.version(),sourcePolicy:'explicit test records, no public provider verification claim',packageHashes:artifacts.map(hash)},null,2));
@@ -0,0 +1,36 @@
// Read-only live acceptance, or a disposable Node registry seeded with the immutable Drupal artifact.
const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict'),crypto=require('node:crypto');
const {chromium}=require(process.env.PLAYWRIGHT_MODULE||'playwright');
const out=path.resolve(process.env.THEME_OUTPUT||'.qa/theme/browser');fs.mkdirSync(out,{recursive:true});
const hash=b=>crypto.createHash('sha256').update(b).digest('hex'),checks=[],errors=[],requests=[],responses=[];
let server,browser,page,temp;const live=process.env.THEME_ORIGIN;
const pass=name=>{checks.push(name);fs.writeFileSync(path.join(out,'progress.json'),JSON.stringify({checks},null,2));console.log('PASS',name);};
const iconClick=async selector=>{const child=page.locator(selector+' svg').first().locator(':scope > *').first();await child.dispatchEvent('click');};
async function shot(name){await page.screenshot({path:path.join(out,name+'.png'),fullPage:true});}
async function fit(){assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'horizontal overflow');}
(async()=>{try{
let origin=live;
if(!origin){temp=fs.mkdtempSync(path.join(os.tmpdir(),'cg-theme-'));server=await require('../../dist/plugins/marketplace').startMarketplaceServer({database:path.join(temp,'registry.db'),publicDirectory:path.resolve('marketplace/public'),officialArtifact:path.resolve('dist/extensions/drupal.cgext')});origin='http://127.0.0.1:'+server.port;}
browser=await chromium.launch({executablePath:process.env.CHROME_PATH||'/opt/google/chrome/chrome',headless:true,args:['--no-sandbox']});
page=await browser.newPage({viewport:{width:1440,height:1000},acceptDownloads:true});page.setDefaultTimeout(20000);page.on('pageerror',e=>errors.push(e.message));page.on('console',m=>{if(m.type()==='error')errors.push(m.text());});page.on('request',r=>requests.push({url:r.url(),method:r.method()}));page.on('response',r=>responses.push({url:r.url(),status:r.status()}));
const response=await page.goto(origin);assert.match(response.headers()['content-security-policy'],/script-src 'self'/);await page.locator('[data-tab="official"]').click();await page.locator('.extension-card').waitFor();await page.evaluate(()=>document.fonts.ready);
const computed=await page.evaluate(()=>{const cs=e=>getComputedStyle(document.querySelector(e));return {font:cs('body').fontFamily,mono:cs('.eyebrow').fontFamily,paper:cs('html').backgroundColor,ink:cs('body').color,muted:cs('.card-copy').color,weight:cs('h1').fontWeight,radius:cs('.extension-card').borderRadius,loaded:[...document.fonts].filter(f=>f.status==='loaded').map(f=>({family:f.family,weight:f.weight}))};});
assert.match(computed.font,/Archivo Variable/);assert.match(computed.mono,/IBM Plex Mono/);assert.equal(computed.paper,'rgb(247, 246, 242)');assert.equal(computed.ink,'rgb(22, 21, 15)');assert.equal(computed.muted,'rgb(86, 84, 74)');assert.equal(computed.weight,'800');assert.equal(computed.radius,'0px');assert.ok(computed.loaded.some(f=>f.family==='Archivo Variable'));assert.ok(computed.loaded.some(f=>f.family==='IBM Plex Mono'));pass('actual local fonts, shared paper/ink tokens, heavy heading and square ruled layout');
await fit();await shot('desktop-catalog');
const icons=await page.locator('svg.ui-icon').evaluateAll(all=>all.map(e=>({name:e.dataset.lucide,hidden:e.getAttribute('aria-hidden'),focusable:e.getAttribute('focusable'),stroke:e.getAttribute('stroke'),width:e.getBoundingClientRect().width})));assert.ok(icons.length>10);assert.ok(icons.every(i=>i.hidden==='true'&&i.focusable==='false'&&i.stroke==='currentColor'));assert.ok(icons.filter(i=>i.width>0).length>10);assert.equal(await page.locator('i[data-ui]').count(),0);pass('locally bundled decorative Lucide SVGs render with currentColor and named text controls');
await page.reload();await page.locator('[data-tab="official"]').waitFor();await page.keyboard.press('Tab');assert.equal(await page.evaluate(()=>document.activeElement.className),'skip-link');const focus=await page.locator('.skip-link').evaluate(e=>({outline:getComputedStyle(e).outlineWidth,top:e.getBoundingClientRect().top}));assert.ok(parseFloat(focus.outline)>=2);assert.ok(focus.top>=0);await page.keyboard.press('Enter');assert.equal(await page.evaluate(()=>document.activeElement.id),'app');pass('visible keyboard focus and working skip-to-content link');
await iconClick('#connect');assert.equal(await page.locator('#connect-dialog').evaluate(e=>e.open),true);await shot('desktop-connection');await iconClick('#connect-dialog .close');assert.equal(await page.locator('#connect-dialog').evaluate(e=>e.open),false);pass('nested SVG paths activate connection and close buttons');
await page.locator('#search').fill('no such framework');assert.match(await page.locator('.empty').innerText(),/No extensions match/);assert.equal(await page.locator('.empty svg').count(),1);await page.locator('#search').fill('');
await page.locator('a[href="/extensions/drupal"]').first().click();await iconClick('[data-view-source]');await page.locator('#source-content').waitFor();assert.match(await page.locator('.source-verified').innerText(),/drupal 0.1.1/);assert.match(await page.locator('.source-unverified').innerText(),/Repository correspondence unverified/);
const selected=await page.evaluate(()=>({release:inspection.active.release,file:inspection.file,source:inspection.active.files[inspection.file]}));assert.equal(selected.release.integrity,'e5ed73bee51524585e4f4da1d5017fdee3037a3fa71d8f08edab6c318fedc59a');assert.equal(await page.locator('#source-content').textContent(),selected.source.slice(0,64000));assert.equal(await page.locator('#source-content script,#source-content img').count(),0);pass('nested SVG source action verifies immutable Drupal bytes with truthful legacy provenance and inert text');
const textDownload=page.waitForEvent('download');await iconClick('#source-file-download');const file=await textDownload;const filePath=path.join(out,'inspected-source.txt');await file.saveAs(filePath);assert.equal(fs.readFileSync(filePath,'utf8'),selected.source);
const packageDownload=page.waitForEvent('download');await iconClick('#source-package-download');const pkg=await packageDownload;const pkgPath=path.join(out,'inspected-drupal.cgext');await pkg.saveAs(pkgPath);assert.equal(hash(fs.readFileSync(pkgPath)),selected.release.integrity);pass('nested SVG download actions preserve full text and exact verified package bytes');
await page.locator('#source-content').focus();assert.ok(await page.locator('#source-content').evaluate(e=>parseFloat(getComputedStyle(e).outlineWidth)>=2));await page.locator('#package-source').scrollIntoViewIfNeeded();await shot('desktop-source');await fit();
for(const width of [390,320]){await page.setViewportSize({width,height:844});await page.goto(origin+'/extensions/drupal');await iconClick('[data-view-source]');await page.locator('#source-content').waitFor();await fit();await page.locator('#source-content').scrollIntoViewIfNeeded();if(width===390)await shot('mobile-source');await page.goto(origin);await page.locator('[data-tab="official"]').click();await page.locator('.extension-card').waitFor();await fit();if(width===390)await shot('mobile-catalog');await iconClick('#connect');await fit();if(width===390)await shot('mobile-connection');await iconClick('#connect-dialog .close');}
pass('catalog, detail, source and connection layouts fit 390px and 320px viewports');
await page.goto(origin+'/publish');await page.locator('#publish-form').waitFor();await fit();await shot('mobile-publisher');await page.setViewportSize({width:1440,height:1000});await shot('desktop-publisher');
// Key generation is browser-local only. Never save or log the secret download contents.
const keyDownload=page.waitForEvent('download');await iconClick('#backup-key');const key=await keyDownload;assert.match(key.suggestedFilename(),/publisher.*json/);await key.delete();pass('publisher form matches theme; nested SVG key-backup action works without publishing or retaining credentials');
assert.ok(requests.every(r=>r.method==='GET'&&new URL(r.url).origin===origin),'unexpected request or external asset');assert.deepEqual(errors,[]);assert.ok(responses.every(r=>r.status<400));pass('same-origin assets only, no mutations, no browser/CSP/resource errors');
fs.writeFileSync(path.join(out,'summary.json'),JSON.stringify({success:true,mode:live?'actual-live-read-only':'local-Node-real-package',origin,checks,computed,icons,browser:browser.version(),requests,responses},null,2));
}catch(error){await shot('failure').catch(()=>{});fs.writeFileSync(path.join(out,'failure.json'),JSON.stringify({checks,errors,requests,responses,error:String(error.stack)},null,2));throw error;}finally{await browser?.close();await server?.close();if(temp)fs.rmSync(temp,{recursive:true,force:true});}})().catch(e=>{console.error(e);process.exitCode=1;});