Download tests/browser/check_preview.js from simon-clmtd/universal-dependencies-visualizer: direct link, hf CLI and curl.
- Browser
- Download file 17.2 kB
-
https://huggingface.co/spaces/simon-clmtd/universal-dependencies-visualizer/resolve/main/tests/browser/check_preview.js
- Command line
-
hf download hf://spaces/simon-clmtd/universal-dependencies-visualizer/tests/browser/check_preview.js
-
curl -L -o check_preview.js https://huggingface.co/spaces/simon-clmtd/universal-dependencies-visualizer/resolve/main/tests/browser/check_preview.js
17.2 kB
| async (page) => { | |
| const base = 'http://127.0.0.1:8765/output/playwright/'; | |
| await page.goto(base + 'api.html'); | |
| await page.waitForFunction(() => document.getElementById('status').textContent.startsWith('PASS')); | |
| const parser = await page.locator('#status').textContent(); | |
| await page.goto(base + 'preview.html'); | |
| await page.waitForFunction(() => Array.from(document.querySelectorAll('iframe')).every(f => f.contentWindow.visualizerDiagnostics?.stage === 'ready'), null, {timeout:60000}); | |
| const frames = await page.evaluate(() => Array.from(document.querySelectorAll('iframe'), f => { | |
| const d = f.contentWindow.visualizerDiagnostics; | |
| const svg = f.contentDocument.querySelector('#vis svg'); | |
| return {id:f.id, entities:d.data.entities.length, text:svg.textContent, width:svg.getBBox().width, height:svg.getBBox().height}; | |
| })); | |
| for (const frame of frames) { | |
| if (frame.width <= 0 || frame.height <= 0) throw Error('Empty SVG: ' + frame.id); | |
| } | |
| if (!frames[0].text.includes('nsubj') || !frames[0].text.includes('English')) throw Error('Missing English arc/label'); | |
| if (!frames[1].text.includes('Bücher')) throw Error('Missing Unicode text'); | |
| if (!frames[2].text.includes('</textarea></script>&')) throw Error('Escaping failed'); | |
| for (const id of ['multisentence', 'wrapped']) { | |
| const frame = page.locator('#' + id).contentFrame(); | |
| const rows = await frame.locator('#vis svg > g.text > text').evaluateAll(elements => elements.map(row => ({ | |
| text:row.textContent, | |
| visible:getComputedStyle(row).display !== 'none' && getComputedStyle(row).visibility !== 'hidden' && row.getBoundingClientRect().height > 0 | |
| }))); | |
| if (!rows.length || rows.some(row => !row.visible)) throw Error('Missing word row in ' + id + ': ' + JSON.stringify(rows)); | |
| const lineCount = await frame.locator('#vis svg > g.text tspan').evaluateAll(words => new Set(words.map(word => Math.round(word.getBoundingClientRect().top))).size); | |
| if (lineCount < 2) throw Error('Fixture did not exercise multiple word lines: ' + id); | |
| const expected = id === 'multisentence' ? ['They', 'Bücher', 'überzeugen'] : ['word1', 'word15', 'word30']; | |
| const visibleText = rows.map(row => row.text).join(' '); | |
| if (expected.some(word => !visibleText.includes(word))) throw Error('Missing words in ' + id); | |
| const exported = await frame.locator('body').evaluate(() => { | |
| const prep = getPreparedSVG(); | |
| const svg = new DOMParser().parseFromString(prep.xml, 'image/svg+xml'); | |
| return Array.from(svg.querySelectorAll('g.text > text'), row => ({text:row.textContent, display:row.style.display})); | |
| }); | |
| if (exported.some(row => row.display === 'none') || expected.some(word => !exported.some(row => row.text.includes(word)))) throw Error('Missing exported words in ' + id); | |
| await page.waitForFunction(id => { | |
| const iframe = document.getElementById(id); | |
| return iframe.clientHeight >= iframe.contentDocument.body.getBoundingClientRect().height; | |
| }, id); | |
| await frame.locator('#vis').screenshot({path:'output/playwright/' + id + '.png'}); | |
| } | |
| const overflow = await page.evaluate(() => Array.from(document.querySelectorAll('iframe'), frame => { | |
| const doc = frame.contentDocument, vis = doc.getElementById('vis'); | |
| return {horizontal:frame.id !== 'wrapped' && vis.scrollWidth > vis.clientWidth + 1, vertical:doc.body.getBoundingClientRect().height > frame.clientHeight + 1}; | |
| })); | |
| if (overflow.some(value => value.horizontal || value.vertical)) throw Error('Unexpected default scrollbars: ' + JSON.stringify(overflow)); | |
| const crowded = page.locator('#crowded').contentFrame(); | |
| const suppressed = await crowded.locator('[data-label-marker-suppressed]').evaluateAll(paths => paths.map(p => ({ | |
| start:getComputedStyle(p).markerStart, end:getComputedStyle(p).markerEnd, | |
| extension:Number(p.dataset.labelLineExtension), | |
| moved:Math.abs(p.getPointAtLength(0).x - Number(p.dataset.originalLabelX)), | |
| tip:p.getPointAtLength(p.getTotalLength()).x + ',' + p.getPointAtLength(p.getTotalLength()).y, | |
| originalTip:p.dataset.originalTip | |
| }))); | |
| if (!suppressed.length || suppressed.some(p => p.start !== 'none' || p.end === 'none')) throw Error('Crowded label markers not suppressed correctly'); | |
| if (suppressed.some(p => p.extension <= 0 || Math.abs(p.moved - p.extension) > .01 || p.tip !== p.originalTip)) throw Error('Line extension changed the wrong endpoint'); | |
| const retained = await crowded.locator('.arcs path').evaluateAll(paths => paths.some(p => getComputedStyle(p).markerStart !== 'none' && getComputedStyle(p).markerEnd !== 'none')); | |
| if (!retained) throw Error('All label triangles were removed'); | |
| const tree = page.frames()[1]; | |
| if (await tree.locator('.title').textContent() !== 'Parsing Output of English') throw Error('Output heading missing'); | |
| await tree.getByText('This syntactic analysis was automatically computed and can contain errors.', {exact:true}).waitFor(); | |
| const headerOrder = await tree.evaluate(() => { | |
| const toggle = document.getElementById('toggle-view-controls').getBoundingClientRect(); | |
| const download = document.getElementById('download-svg').getBoundingClientRect(); | |
| return toggle.right <= download.left && Math.abs(toggle.top - download.top) < 5; | |
| }); | |
| if (!headerOrder) throw Error('View button must precede exports in header'); | |
| const layersAndPadding = await tree.evaluate(() => { | |
| const arcs = document.querySelector('#vis .arcs'); | |
| const span = document.querySelector('#vis .span'); | |
| const word = document.querySelector('tspan[data-chunk-id="0"]').getBBox(); | |
| const background = document.querySelector('#vis svg > g.highlight > rect'); | |
| return { | |
| behind:Boolean(arcs.compareDocumentPosition(span) & Node.DOCUMENT_POSITION_FOLLOWING), | |
| x:word.x - Number(background.getAttribute('x')), | |
| y:word.y - Number(background.getAttribute('y')), | |
| expectedX:Configuration.visual.margin.x, | |
| expectedY:Configuration.visual.margin.y | |
| }; | |
| }); | |
| if (!layersAndPadding.behind) throw Error('Arcs must be painted behind POS boxes'); | |
| if (Math.abs(layersAndPadding.x - layersAndPadding.expectedX) > .1 || Math.abs(layersAndPadding.y - layersAndPadding.expectedY) > .1) throw Error('Word background padding does not match POS padding: ' + JSON.stringify(layersAndPadding)); | |
| if (await tree.locator('#zoom-controls').isVisible()) throw Error('Controls should start hidden'); | |
| const zoom = () => tree.evaluate(() => window.visualizerDiagnostics.zoom); | |
| if (await zoom() !== 1.4) throw Error('Initial zoom should be 140%'); | |
| await tree.getByRole('button', {name:'Show view controls'}).click(); | |
| await tree.getByRole('button', {name:'Zoom in', exact:true}).click(); | |
| if (await zoom() !== 1.6) throw Error('Zoom in failed'); | |
| await tree.getByRole('button', {name:'Zoom out', exact:true}).click(); | |
| await tree.getByRole('button', {name:'Zoom out', exact:true}).click(); | |
| if (await zoom() !== 1.2) throw Error('Zoom out failed'); | |
| await tree.getByRole('button', {name:'Reset zoom', exact:true}).click(); | |
| if (await zoom() !== 1.4) throw Error('Reset should restore 140%'); | |
| await tree.getByRole('button', {name:'Enter fullscreen', exact:true}).click(); | |
| await tree.waitForFunction(() => document.fullscreenElement?.id === 'tree-view'); | |
| await tree.getByRole('button', {name:'Exit fullscreen', exact:true}).click(); | |
| await tree.waitForFunction(() => !document.fullscreenElement); | |
| await tree.getByRole('button', {name:'Hide view controls'}).click(); | |
| if (await tree.locator('#zoom-controls').isVisible()) throw Error('Controls did not close'); | |
| const appearance = await page.frames()[1].locator('#vis svg').evaluate(svg => ({ | |
| background: getComputedStyle(svg.querySelector('.background0')).fill, | |
| wordFont: getComputedStyle(svg.querySelector('text')).fontFamily, | |
| tagFont: getComputedStyle(svg.querySelector('.span text')).fontFamily, | |
| arcFont: getComputedStyle(svg.querySelector('.arcs text')).fontFamily, | |
| stylesheetURLs: Array.from(document.querySelectorAll('link[rel="stylesheet"]'), l => l.href).filter(url=>url.includes('jsdelivr')) | |
| })); | |
| if (appearance.background !== 'rgb(255, 255, 255)') throw Error('Tree background must be white'); | |
| if (!appearance.wordFont.includes('Liberation Sans') || !appearance.tagFont.includes('PT Sans Caption') || !appearance.arcFont.includes('PT Sans Caption')) throw Error('Tree font changed'); | |
| if (!appearance.stylesheetURLs.every(url => url.endsWith('.css'))) throw Error('Malformed stylesheet URL'); | |
| const styled = await page.frames()[1].locator('.arcs path').evaluateAll(paths => paths.some(p => getComputedStyle(p).stroke === 'rgb(255, 0, 0)')); | |
| if (!styled) throw Error('Custom visual style missing'); | |
| await tree.locator('rect[data-span-id="S1-T1"]').hover(); | |
| const head = tree.locator('rect[data-span-id="S1-T2"]'); | |
| if (await head.evaluate(rect => getComputedStyle(rect).fill) !== 'rgb(214, 179, 74)') throw Error('Hovered token head not highlighted'); | |
| const wordBackground = await tree.locator('.hover-head-word').evaluateAll(rects => rects.map(rect => getComputedStyle(rect).fill)); | |
| if (!wordBackground.length || wordBackground.some(color => color !== 'rgb(214, 179, 74)')) throw Error('Head word background not yellow'); | |
| await tree.locator('tspan[data-chunk-id="0"]').hover(); | |
| if (!await head.evaluate(rect => rect.classList.contains('hover-head'))) throw Error('Word hover head not highlighted'); | |
| await tree.locator('rect[data-span-id="S1-T2"]').hover(); | |
| if (await tree.locator('.hover-head, .hover-head-word').count()) throw Error('Root should not highlight a head'); | |
| await tree.locator('rect[data-span-id="S1-T3"]').hover(); | |
| if (!await head.evaluate(rect => rect.classList.contains('hover-head'))) throw Error('Other dependent head not highlighted'); | |
| await tree.locator('#toggle-view-controls').hover(); | |
| if (await tree.locator('.hover-head, .hover-head-word').count()) throw Error('Head highlight not cleared'); | |
| await tree.locator('.arcs text').first().hover(); | |
| const hover = await tree.evaluate(() => ({ | |
| active: Array.from(document.querySelectorAll('.arcs .highlight path')).map(p => getComputedStyle(p).strokeWidth), | |
| labels: Array.from(document.querySelectorAll('.arcs .highlight text')).map(t => getComputedStyle(t).stroke), | |
| inactive: Array.from(document.querySelectorAll('.arcs g[data-from]:not(.highlight) path')).map(p => getComputedStyle(p).stroke) | |
| })); | |
| if (!hover.active.length || hover.active.some(w => w !== '1px')) throw Error('Hover thickened arrows'); | |
| if (hover.labels.some(stroke => stroke !== 'none')) throw Error('Hover outlined labels'); | |
| if (!hover.inactive.length || hover.inactive.some(color => color !== 'rgb(196, 203, 211)')) throw Error('Unrelated dependencies not dimmed'); | |
| const inactiveBorders = await tree.locator('.span:not(.highlight) rect[data-span-id]:not(.hover-head)').evaluateAll(rects => rects.map(rect => getComputedStyle(rect).stroke)); | |
| if (!inactiveBorders.length || inactiveBorders.some(color => color !== 'rgb(196, 203, 211)')) throw Error('Inactive POS borders do not match arrows'); | |
| const markers = await tree.evaluate(() => Array.from(document.querySelectorAll('.arcs g[data-from]:not(.highlight) path')).flatMap(path => { | |
| return ['marker-start', 'marker-end'].flatMap(property => { | |
| const reference = getComputedStyle(path).getPropertyValue(property); | |
| if (reference === 'none') return []; | |
| const id = reference.slice(reference.indexOf('#') + 1).replace(/["')]/g, ''); | |
| const marker = document.getElementById(id); | |
| return Array.from(marker.querySelectorAll('path, polygon, polyline'), shape => getComputedStyle(shape).fill); | |
| }); | |
| })); | |
| if (!markers.length || markers.some(color => color !== 'rgb(196, 203, 211)' && color !== 'none')) throw Error('Inactive arrowheads stayed dark: ' + markers); | |
| await page.screenshot({path:'output/playwright/hover-dimmed.png', fullPage:true}); | |
| await tree.locator('#toggle-view-controls').hover(); | |
| if (await tree.locator('.arcs .highlight').count()) throw Error('Hover state not cleared'); | |
| const restored = await tree.locator('.arcs path').evaluateAll(paths => paths.some(p => getComputedStyle(p).stroke === 'rgb(255, 0, 0)')); | |
| if (!restored) throw Error('Original arc color not restored'); | |
| await page.screenshot({path:'output/playwright/fixtures.png', fullPage:true}); | |
| const exportStyles = await tree.evaluate(() => { | |
| const xml = getPreparedSVG().xml; | |
| const exported = new DOMParser().parseFromString(xml, 'image/svg+xml'); | |
| const source = document.querySelector('.span rect[data-span-id]'); | |
| const saved = exported.querySelector('.span rect[data-span-id]'); | |
| const shadowSource = document.querySelector('.shadow_AnnotatorNotes'); | |
| const shadowSaved = exported.querySelector('.shadow_AnnotatorNotes'); | |
| const arcSource = document.querySelector('.arcs path'); | |
| const arcSaved = exported.querySelector('.arcs path'); | |
| return { | |
| border:getComputedStyle(source).stroke === saved.style.stroke, | |
| width:getComputedStyle(source).strokeWidth === saved.style.strokeWidth, | |
| shadow:getComputedStyle(shadowSource).fill === shadowSaved.style.fill, | |
| arc:getComputedStyle(arcSource).stroke === arcSaved.style.stroke, | |
| font:exported.querySelector('.span text').style.fontFamily, | |
| embedded:xml.includes('@font-face'), | |
| hover:exported.querySelectorAll('.hover-head, .hover-head-word').length | |
| }; | |
| }); | |
| if (!exportStyles.border || !exportStyles.width || !exportStyles.shadow || !exportStyles.arc || !exportStyles.embedded || exportStyles.hover || !exportStyles.font.includes('PT Sans Caption')) throw Error('Export differs from rendered CSS: ' + JSON.stringify(exportStyles)); | |
| for (const extension of ['svg', 'png']) { | |
| const pending = page.waitForEvent('download'); | |
| await page.frames()[1].getByRole('button', {name:'Download ' + extension.toUpperCase()}).click(); | |
| const download = await pending; | |
| await download.saveAs('output/playwright/tree.' + extension); | |
| } | |
| const panel = page.locator('#vis-model-a'); | |
| const resizeHandle = tree.getByRole('button', {name:'Resize tree panel', exact:true}); | |
| const initialPanel = await panel.boundingBox(); | |
| const otherPanel = await page.locator('#vis-model-b').boundingBox(); | |
| const grip = await resizeHandle.boundingBox(); | |
| await page.mouse.move(grip.x + grip.width / 2, grip.y + grip.height / 2); | |
| await page.mouse.down(); | |
| await page.mouse.move(grip.x + grip.width / 2 - 100, grip.y + grip.height / 2 + 80, {steps:8}); | |
| await page.mouse.up(); | |
| const draggedPanel = await panel.boundingBox(); | |
| if (Math.abs(draggedPanel.width - (initialPanel.width - 100)) > 3 || Math.abs(draggedPanel.height - (initialPanel.height + 80)) > 3) throw Error('Panel drag resize failed'); | |
| await resizeHandle.focus(); | |
| await resizeHandle.press('ArrowDown'); | |
| await tree.getByRole('button', {name:'Show view controls'}).click(); | |
| await tree.getByRole('button', {name:'Zoom in', exact:true}).click(); | |
| const retainedPanel = await panel.boundingBox(); | |
| if (Math.abs(retainedPanel.height - (draggedPanel.height + 20)) > 3) throw Error('Auto sizing overrode manual height'); | |
| const untouchedPanel = await page.locator('#vis-model-b').boundingBox(); | |
| if (Math.abs(untouchedPanel.height - otherPanel.height) > 3 || Math.abs(untouchedPanel.width - otherPanel.width) > 3) throw Error('Resizing changed the other tree'); | |
| await resizeHandle.dblclick(); | |
| const resetPanel = await panel.boundingBox(); | |
| if (Math.abs(resetPanel.width - initialPanel.width) > 3) throw Error('Panel fit reset failed'); | |
| const failures = []; | |
| for (const pattern of ['**/conllu.js', '**/lib/brat/visualizer.js']) { | |
| await page.route(pattern, route => route.abort()); | |
| await page.goto(base + 'preview.html'); | |
| await page.waitForFunction(() => Array.from(document.querySelectorAll('iframe')).every(f => f.contentWindow.visualizerDiagnostics?.stage === 'error'), null, {timeout:60000}); | |
| const errors = await page.evaluate(() => Array.from(document.querySelectorAll('iframe'), f => ({text:f.contentDocument.getElementById('status').textContent, disabled:Array.from(f.contentDocument.querySelectorAll('button')).every(b=>b.disabled)}))); | |
| if (!errors.every(e => e.disabled && e.text.includes('Could not load'))) throw Error('Failure not surfaced'); | |
| failures.push({pattern, frames:errors.length}); | |
| await page.unroute(pattern); | |
| } | |
| return {parser, frames, appearance, styled, failures}; | |
| } | |