Loading the parity record
Fetching the measured run — the pinned upstream oracle, every symbol and every case.
Fetching the measured run — the pinned upstream oracle, every symbol and every case.
Parity / puppeteer
Every number on this page was produced by running both implementations over the same cases: the real cheerio package pinned at 1.0.0 answers first, and its answer is the expectation the Go port is held to. Nothing is a hand-written expectation, so a new upstream release re-scores the port on its own. See puppeteer for the port's own documentation. Source: github.com/malcolmston/puppeteer.
Not a generic diagram: every node below names an artefact of this harness — the pinned package it installed, the runner files it started, the case files it streamed, and the counts it wrote out.
| Case group | Cases | Match | Mismatch | Group parity |
|---|---|---|---|---|
| selectors | 97 | 94 | 0 | 96.9% |
| parsing | 46 | 41 | 5 | 89.1% |
| forms | 30 | 26 | 0 | 86.7% |
| http | 24 | 24 | 0 | 100.0% |
Every exported symbol of the upstream package, and what the port offers for it. The upstream list is derived mechanically, never from a README: Produced with `GOWORK=off go doc -all github.com/malcolmston/puppeteer`.. A symbol with no case is untested, never a match.
| Upstream symbol | Go symbol | Status | Cases | Note |
|---|---|---|---|---|
| Page.$ | puppeteer.Page.QuerySelector | differs | matches-*, closest-* | selector engine gaps, see Findings |
| Page.$$ | puppeteer.Page.QuerySelectorAll | differs | all sel-*/attr-*/nth-*/pseudo-* | selector engine gaps, see Findings |
| Page.$$eval | — | missing | — | |
| Page.$eval | — | missing | — | |
| Page.accessibility | — | missing | — | |
| Page.addScriptTag | — | missing | — | |
| Page.addStyleTag | — | missing | — | |
| Page.authenticate | — | missing | — | |
| Page.bringToFront | — | missing | — | |
| Page.browser | — | missing | — | |
| Page.browserContext | — | missing | — | |
| Page.click | — | missing | — | |
| Page.close | — | missing | — | |
| Page.content | puppeteer.Page.Content | match | http-page-content | Go returns the raw body; NormalizedHTML is the closer analogue of page.content() |
| Page.cookies | puppeteer.Page.Cookies | untested | — | jar behaviour is covered through Goto instead |
| Page.coverage | — | missing | — | |
| Page.createCDPSession | — | missing | — | |
| Page.createPDFStream | — | missing | — | |
| Page.deleteCookie | — | missing | — | |
| Page.emulate | — | missing | — | |
| Page.emulateCPUThrottling | — | missing | — | |
| Page.emulateIdleState | — | missing | — | |
| Page.emulateMediaFeatures | — | missing | — | |
| Page.emulateMediaType | — | missing | — | |
| Page.emulateNetworkConditions | — | missing | — | |
| Page.emulateTimezone | — | missing | — | |
| Page.emulateVisionDeficiency | — | missing | — | |
| Page.evaluate | — | missing | — | |
| Page.evaluateHandle | — | missing | — | |
| Page.exposeFunction | — | missing | — | |
| Page.focus | — | missing | — | |
| Page.frames | — | missing | — | |
| Page.getDefaultNavigationTimeout | — | missing | — | |
| Page.getDefaultTimeout | — | missing | — | |
| Page.goBack | puppeteer.Page.GoBack | untested | — | |
| Page.goForward | puppeteer.Page.GoForward | untested | — | |
| Page.goto | puppeteer.Page.Goto / GotoContext | match | http-status-*, http-redirect-*, http-relative-goto-* | HTTP only: no lifecycle events, no subresources, no JS |
| Page.hover | — | missing | — | |
| Page.isClosed | — | missing | — | |
| Page.isDragInterceptionEnabled | — | missing | — | |
| Page.isJavaScriptEnabled | — | missing | — | |
| Page.isServiceWorkerBypassed | — | missing | — | |
| Page.keyboard | — | missing | — | |
| Page.locator | — | missing | — | |
| Page.mainFrame | — | missing | — | |
| Page.metrics | — | missing | — | |
| Page.mouse | — | missing | — | |
| Page.pdf | — | missing | — | |
| Page.queryObjects | — | missing | — | |
| Page.reload | puppeteer.Page.Reload | untested | — | |
| Page.removeExposedFunction | — | missing | — | |
| Page.removeScriptToEvaluateOnNewDocument | — | missing | — | |
| Page.screencast | — | missing | — | |
| Page.screenshot | — | missing | — | |
| Page.select | — | missing | — | |
| Page.setBypassCSP | — | missing | — | |
| Page.setBypassServiceWorker | — | missing | — | |
| Page.setCacheEnabled | — | missing | — | |
| Page.setContent | — | missing | — | |
| Page.setCookie | — | missing | — | |
| Page.setDefaultNavigationTimeout | — | missing | — | |
| Page.setDefaultTimeout | — | missing | — | |
| Page.setDragInterception | — | missing | — | |
| Page.setExtraHTTPHeaders | puppeteer.Page.SetExtraHTTPHeaders | untested | — | |
| Page.setGeolocation | — | missing | — | |
| Page.setJavaScriptEnabled | — | missing | — | |
| Page.setOfflineMode | — | missing | — | |
| Page.setRequestInterception | — | missing | — | |
| Page.setUserAgent | puppeteer.Page.SetUserAgent | untested | — | |
| Page.setViewport | — | missing | — | |
| Page.tap | — | missing | — | |
| Page.target | — | missing | — | |
| Page.title | puppeteer.Page.Title | match | title-*, http-page-title | |
| Page.touchscreen | — | missing | — | |
| Page.tracing | — | missing | — | |
| Page.type | — | missing | — | |
| Page.url | puppeteer.Page.URL | match | http-redirect-*, http-relative-goto-* | |
| Page.viewport | — | missing | — | |
| Page.waitForDevicePrompt | — | missing | — | |
| Page.waitForFileChooser | — | missing | — | |
| Page.waitForFrame | — | missing | — | |
| Page.waitForFunction | — | missing | — | |
| Page.waitForNavigation | — | missing | — | |
| Page.waitForNetworkIdle | — | missing | — | |
| Page.waitForRequest | — | missing | — | |
| Page.waitForResponse | — | missing | — | |
| Page.waitForSelector | puppeteer.Page.WaitForSelector | untested | — | re-fetches the URL on a timer; it cannot observe a DOM change |
| Page.workers | — | missing | — | |
| Frame.$ | — | missing | — | |
| Frame.$$ | — | missing | — | |
| Frame.$$eval | — | missing | — | |
| Frame.$eval | — | missing | — | |
| Frame.addScriptTag | — | missing | — | |
| Frame.addStyleTag | — | missing | — | |
| Frame.childFrames | — | missing | — | |
| Frame.click | — | missing | — | |
| Frame.content | — | missing | — | |
| Frame.detached | — | missing | — | |
| Frame.evaluate | — | missing | — | |
| Frame.evaluateHandle | — | missing | — |
Every case the harness streamed to both runners, with the exact upstream symbol and Go symbol it exercised. A deliberate, documented difference is a deviation and is counted apart from a mismatch.
| Case | Group | Upstream symbol | Go symbol | Status | Note |
|---|---|---|---|---|---|
| form-serialize-basic | forms | cheerio .serialize() | puppeteer.Form.Values().Encode | match | the full control zoo: text, hidden, checkbox, radio, submit, reset, file, disabled, nameless, textarea, select, button |
| form-serialize-get | forms | cheerio .serialize() | puppeteer.Form.Values().Encode | match | method=GET with an action that already carries a query string |
| form-serialize-multipart | forms | cheerio .serialize() | puppeteer.Form.Values().Encode | match | the form declares enctype=multipart/form-data |
| form-serialize-submit-only | forms | cheerio .serialize() | puppeteer.Form.Values().Encode | match | a submit button with a name and a value |
| form-serialize-select-multiple | forms | cheerio .serialize() | puppeteer.Form.Values().Encode | deviation | cheerio@1.0.0 select.val() returns the option's TEXT (M1) not its value attribute (m1) — a known cheerio bug; the port correctly uses the value attribute, so the multiplicity now matches but the value axis cannot without reproducing the bug. |
| form-serialize-order | forms | cheerio .serialize() | puppeteer.Form.Values().Encode | match | field order: document order vs sorted |
| form-serialize-noaction | forms | cheerio .serialize() | puppeteer.Form.Values().Encode | match | no action attribute: submit to the document URL |
| form-serialize-novalue | forms | cheerio .serialize() | puppeteer.Form.Values().Encode | match | controls with no value attribute |
| form-fields-basic | forms | cheerio .serializeArray() | puppeteer.Form.Values | match | which controls are included (sorted on both sides, so order is not under test here) |
| form-fields-get | forms | cheerio .serializeArray() | puppeteer.Form.Values | match | which controls are included (sorted on both sides, so order is not under test here) |
| form-fields-multipart | forms | cheerio .serializeArray() | puppeteer.Form.Values | match | which controls are included (sorted on both sides, so order is not under test here) |
| form-fields-submit-only | forms | cheerio .serializeArray() | puppeteer.Form.Values | match | which controls are included (sorted on both sides, so order is not under test here) |
| form-fields-select-multiple | forms | cheerio .serializeArray() | puppeteer.Form.Values | deviation | cheerio@1.0.0 select.val() returns the option's TEXT (M1) not its value attribute (m1) — a known cheerio bug; the port correctly uses the value attribute. |
| form-fields-order | forms | cheerio .serializeArray() | puppeteer.Form.Values | match | which controls are included (sorted on both sides, so order is not under test here) |
| form-fields-noaction | forms | cheerio .serializeArray() | puppeteer.Form.Values | match | which controls are included (sorted on both sides, so order is not under test here) |
| form-fields-novalue | forms | cheerio .serializeArray() | puppeteer.Form.Values | match | which controls are included (sorted on both sides, so order is not under test here) |
| form-request-basic | forms | HTML form submission algorithm (method/enctype/action) | puppeteer.Form.BuildRequest | match | the full control zoo: text, hidden, checkbox, radio, submit, reset, file, disabled, nameless, textarea, select, button |
| form-request-get | forms | HTML form submission algorithm (method/enctype/action) | puppeteer.Form.BuildRequest | match | method=GET with an action that already carries a query string |
| form-request-multipart | forms | HTML form submission algorithm (method/enctype/action) | puppeteer.Form.BuildRequest | match | the form declares enctype=multipart/form-data |
| form-request-submit-only | forms | HTML form submission algorithm (method/enctype/action) | puppeteer.Form.BuildRequest | match | a submit button with a name and a value |
| form-request-select-multiple | forms | HTML form submission algorithm (method/enctype/action) | puppeteer.Form.BuildRequest | deviation | cheerio@1.0.0 select.val() returns the option's TEXT (M1) not its value attribute (m1) — a known cheerio bug; the port correctly uses the value attribute. |
| form-request-order | forms | HTML form submission algorithm (method/enctype/action) | puppeteer.Form.BuildRequest | match | field order: document order vs sorted |
| form-request-noaction | forms | HTML form submission algorithm (method/enctype/action) | puppeteer.Form.BuildRequest | match | no action attribute: submit to the document URL |
| form-request-novalue | forms | HTML form submission algorithm (method/enctype/action) | puppeteer.Form.BuildRequest | match | controls with no value attribute |
| form-submit-basic | forms | fetch(action, {method, enctype, body}) | puppeteer.Form.Submit | match | end-to-end: the server echoes method, content-type, query and raw body |
| form-submit-get | forms | fetch(action, {method, enctype, body}) | puppeteer.Form.Submit | match | end-to-end: the server echoes method, content-type, query and raw body |
| form-submit-order | forms | fetch(action, {method, enctype, body}) | puppeteer.Form.Submit | match | end-to-end: the server echoes method, content-type, query and raw body |
| form-submit-submit-only | forms | fetch(action, {method, enctype, body}) | puppeteer.Form.Submit | match | end-to-end: the server echoes method, content-type, query and raw body |
| form-submit-novalue | forms | fetch(action, {method, enctype, body}) | puppeteer.Form.Submit | match | end-to-end: the server echoes method, content-type, query and raw body |
| form-submit-select-multiple | forms | fetch(action, {method, enctype, body}) | puppeteer.Form.Submit | deviation | cheerio@1.0.0 select.val() returns the option's TEXT (M1) not its value attribute (m1) — a known cheerio bug; the port correctly uses the value attribute. |
| http-status-200 | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | |
| http-status-201 | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | |
| http-status-404 | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | a 4xx must not be reported as an error |
| http-status-500 | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | |
| http-status-204 | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | no body at all |
| http-redirect-absolute | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | 302 to an absolute URL |
| http-redirect-relative | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | 302 with a relative Location (../final) |
| http-redirect-pathonly | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | 302 with a root-relative Location |
| http-redirect-permanent | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | 301 |
| http-redirect-chain | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | three hops |
| http-redirect-no-follow | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | redirect following disabled |
| http-relative-goto-dotdot | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | Goto('../other') resolved against the current URL |
| http-relative-goto-sibling | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | Goto('sibling') |
| http-relative-goto-absolute-path | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | |
| http-links-resolved | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | document.links semantics: a[href] and area[href], resolved and de-duplicated |
| http-cookie-roundtrip | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | Set-Cookie stored in the jar and replayed on the next navigation |
| http-cookie-two | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | two Set-Cookie headers on one response |
| http-cookie-path-excluded | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | a Path=/scoped cookie must not be sent to /cookie/echo |
| http-cookie-path-included | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | |
| http-cookie-none | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | |
| http-echo-get-query | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | query string is preserved verbatim |
| http-page-title | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | |
| http-page-content | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | raw response body |
| http-page-html | http | fetch/URL/tough-cookie | puppeteer.Page.Goto | match | re-serialised DOM |
| parse-table-implied-tbody | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | tree construction must decide whether <tbody> is synthesised |
| parse-table-foster | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | foster parenting: <b> must move out of the table |
| parse-table-mixed | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | caption/thead/tfoot plus an implied tbody row |
| parse-table-nested | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | |
| parse-table-unclosed-cells | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | implied </td> and </tr> |
| parse-unclosed-p | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | <p> and <div> both close an open <p> |
| parse-unclosed-li | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | |
| parse-unclosed-dl | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | <dt>/<dd> implied end tags |
| parse-misnested-format | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | mismatch | adoption agency algorithm |
| parse-misnested-deep | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | mismatch | adoption agency across a block boundary |
| parse-stray-end-tags | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | unmatched end tags must be ignored |
| parse-raw-text | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | script/style/textarea/title raw-text modes |
| parse-void-elements | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | the void element set |
| parse-entities | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | character reference decoding and re-escaping |
| parse-entities-attr | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | character references inside attribute values |
| parse-attr-syntax | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | unquoted/single-quoted/valueless attributes, upper-case names |
| parse-attr-dup | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | duplicate attribute: first wins |
| parse-case-folding | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | tag and attribute names are lower-cased |
| parse-comments-doctype | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | mismatch | comments and the doctype node |
| parse-self-closing-nonvoid | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | <span/> is not self-closing in HTML |
| parse-select-in-p | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | <table> closes an open <p> |
| parse-text-only | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | |
| parse-empty-doc | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | empty body |
| parse-escape-out | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | escaping on serialisation |
| parse-header-nested | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | |
| parse-full-document | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | a complete document with doctype/head/body |
| parse-nbsp-text | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | non-breaking space: literal or on output? |
| parse-cdata-bogus | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | bogus comments: CDATA, processing instruction, <!bogus> |
| parse-unclosed-tags | parsing | cheerio.load($.html()) | puppeteer.Page.HTML | match | EOF with three open elements |
| tree-table-implied | parsing | cheerio contents() | puppeteer.Node.Children | match | children of <table>: is a tbody inserted? |
| tree-foster | parsing | cheerio contents() | puppeteer.Node.Children | match | top-level nodes: does <b> escape the table? |
| tree-select-in-p | parsing | cheerio contents() | puppeteer.Node.Children | match | |
| tree-void | parsing | cheerio contents() | puppeteer.Node.Children | match | the void element set as child nodes |
| tree-comments-doctype | parsing | cheerio contents() | puppeteer.Node.Children | mismatch | doctype/comment node kinds at top level |
| tree-misnested-deep | parsing | cheerio contents() | puppeteer.Node.Children | mismatch | |
| tree-unclosed-li | parsing | cheerio contents() | puppeteer.Node.Children | match | |
| tree-raw-text | parsing | cheerio contents() | puppeteer.Node.Children | match | |
| tree-full-document | parsing | cheerio contents() | puppeteer.Node.Children | match | |
| text-entities | parsing | cheerio .text() | puppeteer.Page.TextContent | match | decoded text of a run of character references |
| text-escape-out | parsing | cheerio .text() | puppeteer.Page.TextContent | match | |
| text-nbsp | parsing | cheerio .text() | puppeteer.Page.TextContent | match | |
| text-title | parsing | cheerio .text() | puppeteer.Page.TextContent | match | |
| text-body | parsing | cheerio .text() | puppeteer.Page.TextContent | match | |
| title-full-document | parsing | cheerio $('title').text() | puppeteer.Page.Title | match | |
| title-raw-text | parsing | cheerio $('title').text() | puppeteer.Page.Title | match | |
| title-text-only | parsing | cheerio $('title').text() | puppeteer.Page.Title | match |
A case can show the port is less safe than the library it ports. Those findings are written into security.json and filed as draft advisories, never as public issues with a working repro.
| Finding | Severity | Affected | Cases | Summary |
|---|---|---|---|---|
| GHSA-p5fp-ww38-qwfc | high | <= 0.2.0 | escapable-rawtext-title, escapable-rawtext-textarea | `<title>`/`<textarea>` content is serialised verbatim like raw-text `<script>`/`<style>`, so decoded character references re-emit as live markup (sanitizer bypass) |