The Chrome Extension publicSuffix API for QA creates a useful new test surface: an extension can ask the browser to classify a hostname using its Public Suffix List instead of shipping its own parser. That sounds simple until the inputs include co.uk, internal hostnames, IPv6, internationalized domains, unsupported browser builds, or Unicode that looks deceptively familiar.
This tutorial builds a read-only Manifest V3 test extension, defines an independent oracle, and exercises the failure paths that matter to QA engineers and SDETs. The goal is not to turn suffix data into a security verdict. It is to prove that domain classification is predictable, observable, and safely separated from authorization.
browser.publicSuffix, but the API reference currently labels chrome.publicSuffix as Pending. Confirm the API in the exact installed build and feature-detect it. Do not describe it as universally available.What the API exposes
The official reference documents the publicSuffix permission and three methods:
getDomain(hostname, options)returns a registrable-domain result orundefined.getKnownSuffix(hostname)returns a suffix known to the browser’s Public Suffix List orundefined.isKnownSuffix(hostname)answers whether the supplied hostname is a known suffix.
The options are just as important as the methods. allowIPAddress, allowPlainSuffix, and allowUnknownSuffix default to false. The encoding option supports punycode and display; the latter may show Unicode except where unsafe confusables would make that display risky. Treat every option combination as a separate contract.
1. Freeze the QA environment
Before writing assertions, record the evidence needed to reproduce a result:
- Chrome version, channel, operating system, and architecture;
- extension version, extension ID, manifest hash, and service-worker build;
- whether the call used
browserorchrome; - whether the
publicSuffixpermission was present; - method, raw input, exact options, result type, result value, and error;
- oracle fixture version, source date, and checksum;
- the team’s Unicode display policy.
Why freeze the oracle? The browser’s list and your independent Public Suffix List fixture can change at different times. A test that silently downloads a new list is not deterministic. Store a reviewed fixture in the private test repository, version it, and update it deliberately.
2. Build a minimal test extension
Use synthetic hostnames only. This extension does not need cookies, history, tabs, or broad host access.
{
"manifest_version": 3,
"name": "Public Suffix QA Probe",
"version": "0.1.0",
"permissions": ["publicSuffix"],
"background": { "service_worker": "worker.js", "type": "module" }
}
Feature detection is mandatory. Chrome’s namespace guidance says extension APIs are also available under browser from Chrome 148, while chrome remains supported. A DevTools-page extension is an exception: it cannot use the browser namespace in any extension context. The following adapter records which path was selected and makes unsupported behavior explicit.
function resolveApi() {
const browserApi = globalThis.browser?.publicSuffix;
const chromeApi = globalThis.chrome?.publicSuffix;
if (browserApi) return { api: browserApi, namespace: "browser" };
if (chromeApi) return { api: chromeApi, namespace: "chrome" };
return { api: null, namespace: "unavailable" };
}
export async function classify(hostname, options = {}) {
const { api, namespace } = resolveApi();
if (!api) return { status: "unsupported", namespace, hostname, options };
try {
const [domain, suffix, known] = await Promise.all([
Promise.resolve(api.getDomain(hostname, options)),
Promise.resolve(api.getKnownSuffix(hostname)),
Promise.resolve(api.isKnownSuffix(hostname))
]);
return { status: "ok", namespace, hostname, options, domain, suffix, known };
} catch (error) {
return {
status: "error", namespace, hostname, options,
errorName: error?.name ?? "Error",
errorMessage: String(error?.message ?? error)
};
}
}
Do not collapse undefined, an empty string, a thrown error, and an unavailable API into one value. They represent different product behavior and need different assertions.
3. Create a risk-based hostname matrix
Start with a small control set, then add option-specific and adversarial inputs. The values below are test categories, not a substitute for the frozen oracle.
| Class | Example input | What QA should prove |
|---|---|---|
| Ordinary domain | www.example.com |
Registrable domain and suffix are separated consistently. |
| Multi-label suffix | shop.example.co.uk |
The test does not assume the last label alone is the suffix. |
| Plain suffix | co.uk |
Behavior differs only when allowPlainSuffix changes. |
| Unknown internal host | service.qa-internal |
allowUnknownSuffix is tested false and true. |
| IPv4 and IPv6 | 192.0.2.1, 2001:db8::1 |
allowIPAddress is tested false and true. |
| IDN | Punycode and its Unicode form | punycode and display outputs follow the reviewed policy. |
| Normalization | uppercase or trailing dot | The contract is captured rather than guessed. |
| Malformed | empty, whitespace, URL, bad label | Undefined results and errors remain distinguishable. |
Also include localhost, a hostname containing a port, a full URL accidentally sent where a hostname is required, an overlong label, repeated dots, leading and trailing whitespace, and bracketed versus unbracketed IPv6. These cases catch adapters that trim, parse, or normalize inputs without documenting it.
4. Test every option independently
Avoid one giant parameterized test whose failures are hard to diagnose. Use a control row and change one option at a time:
- Run the input with default options.
- Toggle
allowIPAddressonly for the IP rows. - Toggle
allowPlainSuffixonly for suffix-only rows. - Toggle
allowUnknownSuffixonly for internal or unknown suffixes. - Compare
punycodeanddisplayfor the same IDN rows.
For each call, store a structured record. A good assertion compares method, input, option map, result type, result value, browser identity, and oracle identity. A screenshot is useful for review, but the JSON record is the automation evidence.
{
"caseId": "multi-label-default-01",
"hostname": "shop.example.co.uk",
"options": {},
"expectedSource": "psl-fixture-2026-08.sha256",
"browser": { "channel": "test-channel", "build": "record-at-runtime" },
"assert": ["status", "namespace", "domain", "suffix", "known"]
}
5. Validate IDN display safely
Internationalized domains are where visual assertions become dangerous. Test the ASCII punycode representation and the intended display representation as separate fields. Include characters from different scripts, combining marks, right-to-left text, and labels that resemble a familiar Latin hostname. Verify that logs and screenshots preserve the exact code points.
Never make a phishing verdict because a displayed hostname looks familiar. The API’s display behavior and the product’s security policy are different layers. Preserve the canonical ASCII value for machine comparison and use a human-readable rendering only as presentation data.
6. Exercise permissions and unsupported builds
Create two manifests: one with publicSuffix and one without it. Then run on a build where the API is present and a controlled older or unsupported build. Expected outcomes must be explicit:
- present API plus permission;
- present namespace but missing permission;
- missing API on an unsupported build;
browserunavailable whilechromeworks;- DevTools-page packaging where the namespace restriction applies;
- extension update that raises
minimum_chrome_version.
The last case deserves release testing. Chrome’s minimum-version documentation says a new install on an older browser is shown as incompatible, but an existing installation can silently stop receiving later extension updates. Decide whether to use a runtime guard, raise the minimum version, or provide both. Whatever the product choice, test the user-visible message and the update path.
7. Add service-worker and concurrency tests
Manifest V3 service workers can stop between events. Run the same batch before and after a worker restart and compare the records. Repeat identical calls, reverse the input order, and send concurrent batches. The API result should not depend on stale in-memory state, prior options, or completion order.
Useful failure injections include malformed messages to the worker, a closed calling page, extension reload during a batch, an oversized input list, and two clients asking for different encodings at once. Correlate each response with a unique case ID so asynchronous completion cannot attach an answer to the wrong hostname.
8. Drive one end-to-end user flow
Chrome’s official E2E guide recommends loading the built extension in a real browser and testing user-visible behavior with tools such as Puppeteer, Playwright, or Selenium. Keep most matrix checks deterministic and headless, then run one visible workflow:
- Build and load the unpacked extension in a clean profile.
- Open the test panel and confirm the recorded browser and extension identities.
- Submit the reviewed fixture batch.
- Check that unsupported, undefined, error, and successful rows use distinct UI states.
- Export the evidence and compare its checksum with the displayed run ID.
- Restart the worker or browser and repeat the same batch.
Assert behavior, not private implementation state. The evidence should show what input was classified, how it was classified, and why the run was accepted or blocked.
9. Keep the security boundary explicit
A public suffix result is classification data. It must not automatically grant access, relax an origin check, widen a host permission, set a cookie boundary, or declare a site safe. Registrable domains can group related hostnames, but two subdomains are still different origins, and a hostname can still be attacker-controlled.
Add negative security tests that try to misuse the result:
- a lookalike IDN must not inherit trust from its display form;
- an internal unknown suffix must not bypass an allowlist;
- an IP allowed for classification must not become an authorized endpoint;
- a shared registrable domain must not merge tokens or session data across origins;
- an undefined result must fail closed in security-sensitive policy.
Keep exact URL and origin validation in its own reviewed module. If suffix classification informs a policy, make the final decision visible and human-reviewable.
10. Define the release gate
A practical CI gate should fail when the API appears unexpectedly unavailable on the supported build, the permission contract changes, a stable control row differs from the frozen oracle, error and undefined states merge, concurrent results lose correlation, or a security-negative case is accepted. It should warn—not silently rewrite expectations—when a reviewed PSL refresh changes a fixture.
Attach the manifest hash, browser build, fixture checksum, result JSON, failed-case diff, and selected screenshots to the run. A human reviewer should approve any fixture refresh, browser support change, Unicode policy change, or use of the result in security-sensitive logic.
QA checklist
- Confirm the API exists in the installed Chrome build.
- Record namespace and
publicSuffixpermission state. - Freeze an independent PSL oracle and checksum.
- Cover ordinary, multi-label, plain, unknown, IP, IDN, and malformed inputs.
- Test each option independently and distinguish undefined from errors.
- Repeat after service-worker restart and under concurrent batches.
- Validate the visible workflow in a clean browser profile.
- Keep classification separate from authorization and origin security.
- Review minimum-version and extension-update behavior.
- Require human approval for oracle and policy changes.
Official references
- What’s new in Chrome extensions
- Chrome publicSuffix API reference
- Browser namespace guidance
- Minimum Chrome version
- End-to-end testing Chrome extensions
Final takeaway
The Chrome Extension publicSuffix API for QA is best tested as a versioned classifier with explicit options, an independent oracle, and strong unsupported-build evidence. Cover the hostname shapes users actually encounter, preserve canonical values for machine comparison, and keep every authorization decision outside the classifier. That gives teams useful domain-boundary data without turning a convenience API into an accidental security boundary.
