Commits (42)
# html-validate changelog
## [4.11.0](https://gitlab.com/html-validate/html-validate/compare/v4.10.1...v4.11.0) (2021-05-08)
### Features
- `dom:ready` and `dom:load` contains `source` reference ([430ec7c](https://gitlab.com/html-validate/html-validate/commit/430ec7c611ce5f09dfaa7e1e08febe756ee87db1)), closes [#115](https://gitlab.com/html-validate/html-validate/issues/115)
- add `:scope` pseudoselector ([6e3d837](https://gitlab.com/html-validate/html-validate/commit/6e3d83713ab74297a4b4af41f6b244c9e3d7822a)), closes [#114](https://gitlab.com/html-validate/html-validate/issues/114)
- add `isSameNode()` ([7d99007](https://gitlab.com/html-validate/html-validate/commit/7d99007b9458d2ff1ca37aae756dd2806837ca68))
- add new event `source:ready` ([4c1d115](https://gitlab.com/html-validate/html-validate/commit/4c1d115594f0eebdccfcbe6a6518a805b4a26929)), closes [#115](https://gitlab.com/html-validate/html-validate/issues/115)
- **rules:** `deprecated` takes `include` and `exclude` options ([e00d7c1](https://gitlab.com/html-validate/html-validate/commit/e00d7c161bf7244931378f51b3c481016d49aad6))
### Bug Fixes
- **dom:** throw if `tagName` is invalid ([42d7100](https://gitlab.com/html-validate/html-validate/commit/42d710020eb3c0e4d2e528859ed23a56095feb87))
### [4.10.1](https://gitlab.com/html-validate/html-validate/compare/v4.10.0...v4.10.1) (2021-04-25)
### Bug Fixes
......
---
docType: content
title: API - Source
id: api:Source
name: Source
---
# `Source`
Source interface.
HTML source with file, line and column context.
Optional hooks can be attached.
This is usually added by transformers to postprocess.
```ts
interface Source {
data: string;
filename: string;
/**
* Line in the original data.
*
* Starts at 1 (first line).
*/
line: number;
/**
* Column in the original data.
*
* Starts at 1 (first column).
*/
column: number;
/**
* Offset in the original data.
*
* Starts at 0 (first character).
*/
offset: number;
/**
* Original data. When a transformer extracts a portion of the original source
* this must be set to the full original source.
*
* Since the transformer might be chained always test if the input source
* itself has `originalData` set, e.g.:
*
* `originalData = input.originalData || input.data`.
*/
originalData?: string;
/**
* Hooks for processing the source as it is being parsed.
*/
hooks?: SourceHooks;
/**
* Internal property to keep track of what transformers has run on this
* source. Entries are in reverse-order, e.g. the last applied transform is
* first.
*/
transformedBy?: string[];
}
```
## `SourceHooks`
```
interface SourceHooks {
/**
* Called for every attribute.
*
* The original attribute must be yielded as well or no attribute will be
* added.
*
* @returns Attribute data for an attribute to be added to the element.
*/
processAttribute?: ProcessAttributeCallback | null;
/**
* Called for every element after element is created but before any children.
*
* May modify the element.
*/
processElement?: ProcessElementCallback | null;
}
```
......@@ -18,6 +18,20 @@ title: Events
Emitted after after configuration is ready but before DOM is initialized.
### `source:ready`
```typescript
{
source: Source;
}
```
Emitted after after source is transformed but before DOM is initialized.
See {@link api:Source} for data structure.
The source object must not be modified (use a transformer if modifications are required).
The `hooks` property is always unset.
### `token`
```typescript
......@@ -36,17 +50,19 @@ Emitted for each lexer token during parsing.
```typescript
{
source: Source;
}
```
Emitted after initialization but before tokenization and parsing occurs. Can be
used to initialize state in rules.
Emitted after initialization but before tokenization and parsing occurs.
Can be used to initialize state in rules.
### `dom:ready`
```typescript
{
document: DOMTree,
source: Source;
}
```
......
const chalk = require("chalk");
const kleur = require("kleur");
const HtmlValidate = require("../../../../dist/htmlvalidate").default;
const codeframe = require("../../../../dist/formatters/codeframe").codeframe;
......@@ -15,8 +15,8 @@ module.exports = function generateValidationResultsProcessor(log, validateMap) {
};
function $process() {
const oldLevel = chalk.level;
chalk.level = 0;
const previousEnabled = kleur.enabled;
kleur.enabled = false;
validateMap.forEach((validation) => {
htmlvalidate = new HtmlValidate(validation.config);
validation.report = htmlvalidate.validateString(validation.markup);
......@@ -26,6 +26,6 @@ module.exports = function generateValidationResultsProcessor(log, validateMap) {
validation.showResults = !validation.report.valid;
}
});
chalk.level = oldLevel;
kleur.enabled = previousEnabled;
}
};
......@@ -40,3 +40,27 @@ The message will be shown alongside the regular message:
<validate name="custom-message" rules="deprecated" elements="deprecated.json">
<my-element>...</my-element>
</validate>
## Options
This rule takes an optional object:
```javascript
{
"include": [],
"exclude": [],
}
```
### `include`
If set only elements listed in this array generates errors.
### `exclude`
If set elements listed in this array is ignored.
## Version history
- v4.11.0 - `include` and `exclude` options added.
- v1.13.0 - Rule added.
This diff is collapsed.
{
"name": "html-validate",
"version": "4.10.1",
"version": "4.11.0",
"description": "html linter",
"keywords": [
"html",
......@@ -51,7 +51,8 @@
"prettier:check": "prettier --check .",
"prettier:write": "prettier --write .",
"start": "grunt connect",
"test": "jest --ci"
"test": "jest --ci",
"version": "scripts/version"
},
"commitlint": {
"extends": [
......@@ -121,46 +122,46 @@
"@sidvind/better-ajv-errors": "^0.8.0",
"acorn-walk": "^8.0.0",
"ajv": "^7.0.0",
"chalk": "^4.0.0",
"deepmerge": "^4.2.0",
"espree": "^7.3.0",
"glob": "^7.1.0",
"ignore": "^5.0.0",
"json-merge-patch": "^1.0.0",
"kleur": "^4.1.0",
"minimist": "^1.2.0",
"prompts": "^2.0.0"
},
"devDependencies": {
"@babel/core": "7.13.16",
"@babel/preset-env": "7.13.15",
"@babel/core": "7.14.0",
"@babel/preset-env": "7.14.1",
"@commitlint/cli": "12.1.1",
"@html-validate/commitlint-config": "1.3.1",
"@html-validate/eslint-config": "4.1.0",
"@html-validate/eslint-config-jest": "4.0.0",
"@html-validate/eslint-config-typescript": "4.0.0",
"@html-validate/eslint-config": "4.4.0",
"@html-validate/eslint-config-jest": "4.4.0",
"@html-validate/eslint-config-typescript": "4.4.0",
"@html-validate/jest-config": "1.2.9",
"@html-validate/prettier-config": "1.1.0",
"@html-validate/semantic-release-config": "1.2.6",
"@html-validate/semantic-release-config": "1.2.13",
"@lodder/grunt-postcss": "3.0.1",
"@types/babar": "0.2.0",
"@types/babel__code-frame": "7.0.2",
"@types/estree": "0.0.47",
"@types/glob": "7.1.3",
"@types/inquirer": "7.3.1",
"@types/jest": "26.0.22",
"@types/jest": "26.0.23",
"@types/json-merge-patch": "0.0.5",
"@types/minimist": "1.2.1",
"@types/node": "11.15.52",
"@types/prompts": "2.0.10",
"@types/node": "11.15.53",
"@types/prompts": "2.0.11",
"autoprefixer": "10.2.5",
"babar": "0.2.0",
"babelify": "10.0.0",
"bootstrap-sass": "3.4.1",
"canonical-path": "1.0.0",
"cssnano": "5.0.1",
"cssnano": "5.0.2",
"dgeni": "0.4.14",
"dgeni-front-matter": "2.0.3",
"dgeni-packages": "0.29.0",
"dgeni-packages": "0.29.1",
"eslint": "7.25.0",
"eslint-plugin-security": "1.4.0",
"eslint-plugin-sonarjs": "0.7.0",
......@@ -177,20 +178,19 @@
"jest": "26.6.3",
"jest-diff": "26.6.2",
"jquery": "3.6.0",
"lint-staged": "10.5.4",
"lint-staged": "11.0.0",
"load-grunt-tasks": "5.1.0",
"marked": "2.0.3",
"minimatch": "3.0.4",
"npm-pkg-lint": "1.3.0",
"postcss": "8.2.12",
"postcss": "8.2.14",
"prettier": "2.2.1",
"pretty-format": "26.6.2",
"sass": "1.32.11",
"sass": "1.32.12",
"semantic-release": "17.4.2",
"serve-static": "1.14.1",
"stringmap": "0.2.2",
"strip-ansi": "6.0.0",
"ts-jest": "26.5.5",
"ts-jest": "26.5.6",
"typescript": "4.2.4"
},
"engines": {
......
#!/bin/bash
shopt -s globstar
for file in docs/**/*.md; do
sed "s/%version%/${npm_new_version}/g" -i "${file}"
git add "${file}"
done
/* eslint-disable no-console, no-process-exit, sonarjs/no-duplicate-string */
import path from "path";
import chalk from "chalk";
import kleur from "kleur";
import minimist from "minimist";
import { TokenDump } from "../engine";
import { SchemaValidationError } from "../error";
......@@ -55,7 +55,7 @@ function lint(files: string[]): Report {
try {
return htmlvalidate.validateFile(filename);
} catch (err) {
console.error(chalk.red(`Validator crashed when parsing "${filename}"`));
console.error(kleur.red(`Validator crashed when parsing "${filename}"`));
throw err;
}
});
......@@ -101,9 +101,9 @@ function renameStdin(report: Report, filename: string): void {
function handleValidationError(err: SchemaValidationError): void {
if (err.filename) {
const filename = path.relative(process.cwd(), err.filename);
console.log(chalk.red(`A configuration error was found in "${filename}":`));
console.log(kleur.red(`A configuration error was found in "${filename}":`));
} else {
console.log(chalk.red(`A configuration error was found:`));
console.log(kleur.red(`A configuration error was found:`));
}
console.group();
{
......@@ -113,7 +113,7 @@ function handleValidationError(err: SchemaValidationError): void {
}
function handleUserError(err: UserError): void {
console.error(chalk.red("Caught exception:"));
console.error(kleur.red("Caught exception:"));
console.group();
{
console.error(err);
......@@ -122,16 +122,16 @@ function handleUserError(err: UserError): void {
}
function handleUnknownError(err: Error): void {
console.error(chalk.red("Caught exception:"));
console.error(kleur.red("Caught exception:"));
console.group();
{
console.error(err);
}
console.groupEnd();
const bugUrl = `${pkg.bugs.url}?issuable_template=Bug`;
console.error(chalk.red(`This is a bug in ${pkg.name}-${pkg.version}.`));
console.error(kleur.red(`This is a bug in ${pkg.name}-${pkg.version}.`));
console.error(
chalk.red(
kleur.red(
[
`Please file a bug at ${bugUrl}`,
`and include this message in full and if possible the content of the`,
......
......@@ -3,7 +3,7 @@ import path from "path";
import { Config } from "./config";
/**
* @hidden
* @internal
*/
interface ConfigClass {
empty(): Config;
......@@ -31,7 +31,7 @@ export class ConfigLoader {
/**
* Flush configuration cache.
*
* @param filename If given only the cache for that file is flushed.
* @param filename - If given only the cache for that file is flushed.
*/
public flush(filename?: string): void {
if (filename) {
......@@ -46,7 +46,7 @@ export class ConfigLoader {
*
* Searches parent directories for configuration and merges the result.
*
* @param filename Filename to get configuration for.
* @param filename - Filename to get configuration for.
*/
public fromTarget(filename: string): Config | null {
if (filename === "inline") {
......
......@@ -224,7 +224,7 @@ export class Config {
* Returns a new configuration as a merge of the two. Entries from the passed
* object takes priority over this object.
*
* @param {Config} rhs - Configuration to merge with this one.
* @param rhs - Configuration to merge with this one.
*/
public merge(rhs: Config): Config {
return new Config(mergeInternal(this.config, rhs.config));
......@@ -298,7 +298,7 @@ export class Config {
}
/**
* @hidden exposed for testing only
* @internal exposed for testing only
*/
public static expandRelative(src: string, currentPath: string): string {
if (src[0] === ".") {
......@@ -310,7 +310,7 @@ export class Config {
/**
* Get a copy of internal configuration data.
*
* @hidden primary purpose is unittests
* @internal primary purpose is unittests
*/
public get(): ConfigData {
const config = { ...this.config };
......@@ -433,7 +433,7 @@ export class Config {
* @param source - Current source to transform.
* @param filename - If set it is the filename used to match
* transformer. Default is to use filename from source.
* @return A list of transformed sources ready for validation.
* @returns A list of transformed sources ready for validation.
*/
public transformSource(source: Source, filename?: string): Source[] {
const transformer = this.findTransformer(filename || source.filename);
......@@ -468,7 +468,7 @@ export class Config {
*
* @param source - Filename to transform (according to configured
* transformations)
* @return A list of transformed sources ready for validation.
* @returns A list of transformed sources ready for validation.
*/
public transformFilename(filename: string): Source[] {
const data = fs.readFileSync(filename, { encoding: "utf8" });
......
......@@ -55,7 +55,7 @@ function sliceSize(size: number, begin: number, end?: number): number {
* properly calculate line and column information. If not given the text is
* assumed to contain no newlines.
*
* @param location Source location
* @param location - Source location
* @param begin - Start location. Default is 0.
* @param end - End location. Default is size of location. Negative values are
* counted from end, e.g. `-2` means `size - 2`.
......
......@@ -20,9 +20,7 @@ export interface SourceHooks {
* The original attribute must be yielded as well or no attribute will be
* added.
*
* @generator
* @yields {AttributeData} Attribute data for an attribute to be added to the
* element.
* @returns Attribute data for an attribute to be added to the element.
*/
processAttribute?: ProcessAttributeCallback | null;
......
......@@ -56,6 +56,13 @@ describe("Attribute", () => {
expect(attr.valueMatches(/foo/)).toBeFalsy();
});
it("should return false for boolean attributes", () => {
expect.assertions(2);
const attr = new Attribute("foo", null, keyLocation, valueLocation);
expect(attr.valueMatches("true")).toBeFalsy();
expect(attr.valueMatches(/any/)).toBeFalsy();
});
it("should match DynamicValue", () => {
expect.assertions(2);
const attr = new Attribute("foo", new DynamicValue("bar"), keyLocation, valueLocation);
......
......@@ -60,11 +60,10 @@ export class Attribute {
/**
* Test attribute value.
*
* @param {RegExp|string} pattern - Pattern to match value against. RegExp or
* a string (===)
* @param {boolean} [dynamicMatches=true] - If true `DynamicValue` will always
* match, if false it never matches.
* @returns {boolean} `true` if attribute value matches pattern.
* @param pattern - Pattern to match value against. RegExp or a string (===)
* @param dynamicMatches - If true `DynamicValue` will always match, if false
* it never matches.
* @returns `true` if attribute value matches pattern.
*/
public valueMatches(pattern: RegExp, dynamicMatches?: boolean): boolean;
public valueMatches(pattern: string, dynamicMatches?: boolean): boolean;
......
......@@ -3,42 +3,48 @@ import { Combinator, parseCombinator } from "./combinator";
describe("DOM Combinator", () => {
it("should default to descendant combinator", () => {
expect.assertions(1);
const result = parseCombinator("");
const result = parseCombinator("", "div");
expect(result).toEqual(Combinator.DESCENDANT);
});
it("should parse > as child combinator", () => {
expect.assertions(1);
const result = parseCombinator(">");
const result = parseCombinator(">", "> div");
expect(result).toEqual(Combinator.CHILD);
});
it("should parse + as adjacent sibling combinator", () => {
expect.assertions(1);
const result = parseCombinator("+");
const result = parseCombinator("+", "+ div");
expect(result).toEqual(Combinator.ADJACENT_SIBLING);
});
it("should parse + as general sibling combinator", () => {
expect.assertions(1);
const result = parseCombinator("~");
const result = parseCombinator("~", "~ div");
expect(result).toEqual(Combinator.GENERAL_SIBLING);
});
it("should parse :scope pseudo class", () => {
expect.assertions(1);
const result = parseCombinator("", ":scope");
expect(result).toEqual(Combinator.SCOPE);
});
it("should handle undefined as descendant", () => {
expect.assertions(1);
const result = parseCombinator(undefined);
const result = parseCombinator(undefined, "div");
expect(result).toEqual(Combinator.DESCENDANT);
});
it("should handle null as descendant", () => {
expect.assertions(1);
const result = parseCombinator(null);
const result = parseCombinator(null, "div");
expect(result).toEqual(Combinator.DESCENDANT);
});
it("should throw error on invalid combinator", () => {
expect.assertions(1);
expect(() => parseCombinator("a")).toThrow();
expect(() => parseCombinator("a", "")).toThrow();
});
});
export enum Combinator {
DESCENDANT,
DESCENDANT = 1,
CHILD,
ADJACENT_SIBLING,
GENERAL_SIBLING,
/* special cases */
SCOPE,
}
export function parseCombinator(combinator: string | undefined | null): Combinator {
export function parseCombinator(
combinator: string | undefined | null,
pattern: string
): Combinator {
/* special case, when pattern is :scope [[Selector]] will handle this
* "combinator" to match itself instead of descendants */
if (pattern === ":scope") {
return Combinator.SCOPE;
}
switch (combinator) {
case undefined:
case null:
......
......@@ -61,6 +61,21 @@ describe("DOMNode", () => {
});
});
describe("isSameNode()", () => {
const a = new DOMNode(NodeType.ELEMENT_NODE, "div", location);
const b = new DOMNode(NodeType.ELEMENT_NODE, "div", location);
it("should return true if the element references the same node", () => {
expect.assertions(1);
expect(a.isSameNode(a)).toBeTruthy();
});
it("should return false if the element is another node", () => {
expect.assertions(1);
expect(a.isSameNode(b)).toBeFalsy();
});
});
describe("firstChild", () => {
it("should return first child if present", () => {
expect.assertions(1);
......@@ -135,7 +150,7 @@ describe("DOMNode", () => {
expect.assertions(1);
const markup = `lorem <i>ipsum</i> <b>dolor <u>sit amet</u></b>`;
const parser = new Parser(Config.empty().resolve());
const doc = parser.parseHtml(markup).root;
const doc = parser.parseHtml(markup);
expect(doc.textContent).toEqual("lorem ipsum dolor sit amet");
});
});
......@@ -247,6 +262,13 @@ describe("DOMNode", () => {
expect(node.cacheRemove("bar")).toBeFalsy();
});
it("cacheRemove() should return false if cache is disabled", () => {
expect.assertions(1);
const node = new DOMNode(NodeType.ELEMENT_NODE, "div", location);
node.cacheSet("foo", 1);
expect(node.cacheRemove("foo")).toBeFalsy();
});
it("cacheExists() should return true if value is cached", () => {
expect.assertions(2);
const node = new DOMNode(NodeType.ELEMENT_NODE, "div", location);
......
......@@ -142,6 +142,13 @@ export class DOMNode {
return this.nodeType === NodeType.DOCUMENT_NODE;
}
/**
* Tests if two nodes are the same (references the same object).
*/
public isSameNode(otherNode: DOMNode): boolean {
return this.unique === otherNode.unique;
}
/**
* Returns a DOMNode representing the first direct child node or `null` if the
* node has no children.
......
......@@ -5,7 +5,7 @@ import { MetaData, MetaElement, MetaTable } from "../meta";
import { Parser } from "../parser";
import { processAttribute } from "../transform/mocks/attribute";
import { DynamicValue } from "./dynamic-value";
import { Attribute, DOMTree, HtmlElement, NodeClosed, NodeType } from ".";
import { Attribute, HtmlElement, NodeClosed, NodeType } from ".";
interface LocationSpec {
column: number;
......@@ -23,7 +23,7 @@ function createLocation({ column, size }: LocationSpec): Location {
}
describe("HtmlElement", () => {
let root: DOMTree;
let document: HtmlElement;
const location = createLocation({ column: 1, size: 4 });
beforeEach(() => {
......@@ -46,7 +46,7 @@ describe("HtmlElement", () => {
processAttribute,
},
};
root = parser.parseHtml(source);
document = parser.parseHtml(source);
});
describe("fromTokens()", () => {
......@@ -225,6 +225,26 @@ describe("HtmlElement", () => {
expect(c.nextSibling).toBeNull();
});
describe("siblings", () => {
it("should return list of siblings", () => {
expect.assertions(1);
expect.assertions(3);
const root = new HtmlElement("root", null, NodeClosed.EndTag, null, location);
const a = new HtmlElement("a", root, NodeClosed.EndTag, null, location);
const b = new HtmlElement("b", root, NodeClosed.EndTag, null, location);
const c = new HtmlElement("c", root, NodeClosed.EndTag, null, location);
expect(a.siblings).toEqual([a, b, c]);
expect(b.siblings).toEqual([a, b, c]);
expect(c.siblings).toEqual([a, b, c]);
});
it("should handle detached elements", () => {
expect.assertions(1);
const element = new HtmlElement("span", null, NodeClosed.EndTag, null, location);
expect(element.siblings).toEqual([element]);
});
});
describe("attributes getter", () => {
it("should return list of all attributes", () => {
expect.assertions(2);
......@@ -405,10 +425,10 @@ describe("HtmlElement", () => {
it("for nodes in a tree", () => {
expect.assertions(4);
expect(root.querySelector("#parent").depth).toEqual(0);
expect(root.querySelector("ul").depth).toEqual(1);
expect(root.querySelector("li.foo").depth).toEqual(2);
expect(root.querySelector("li.bar").depth).toEqual(2);
expect(document.querySelector("#parent").depth).toEqual(0);
expect(document.querySelector("ul").depth).toEqual(1);
expect(document.querySelector("li.foo").depth).toEqual(2);
expect(document.querySelector("li.bar").depth).toEqual(2);
});
});
......@@ -417,13 +437,13 @@ describe("HtmlElement", () => {
beforeAll(() => {
const parser = new Parser(Config.empty().resolve());
root = parser.parseHtml(`
document = parser.parseHtml(`
<div id="1" class="x">
<div id="2" class="x">
<p id="3" class="x"></p>
</div>
</div>`);
node = root.querySelector("p");
node = document.querySelector("p");
});
it("should return first parent matching the selector", () => {
......@@ -476,6 +496,20 @@ describe("HtmlElement", () => {
expect(el.generateSelector()).toEqual("#foo > p");
});
it("should not use id if id is not unique", () => {
expect.assertions(1);
const document = parser.parseHtml(`
<div>
<div id="foo">
<p></p>
</div>
<div id="foo"></div>
</div>
`);
const el = document.querySelector("p");
expect(el.generateSelector()).toEqual("div > div:nth-child(1) > p");
});
it("should handle colon in id", () => {
expect.assertions(1);
const document = parser.parseHtml(`
......@@ -594,7 +628,7 @@ describe("HtmlElement", () => {
describe("getElementsByTagName()", () => {
it("should find elements", () => {
expect.assertions(3);
const nodes = root.getElementsByTagName("li");
const nodes = document.getElementsByTagName("li");
expect(nodes).toHaveLength(2);
expect(nodes[0].getAttributeValue("class")).toEqual("foo");
expect(nodes[1].getAttributeValue("class")).toEqual("bar baz");
......@@ -602,7 +636,7 @@ describe("HtmlElement", () => {
it("should support universal selector", () => {
expect.assertions(2);
const tagNames = root.getElementsByTagName("*").map((cur: HtmlElement) => cur.tagName);
const tagNames = document.getElementsByTagName("*").map((cur: HtmlElement) => cur.tagName);
expect(tagNames).toHaveLength(6);
expect(tagNames).toEqual(["div", "ul", "li", "li", "p", "span"]);
});
......@@ -611,7 +645,7 @@ describe("HtmlElement", () => {
describe("matches()", () => {
it("should return true if element matches given selector", () => {
expect.assertions(3);
const node = root.querySelector("#spam");
const node = document.querySelector("#spam");
expect(node.matches("ul > li")).toBeTruthy();
expect(node.matches("li.baz")).toBeTruthy();
expect(node.matches("#parent li")).toBeTruthy();
......@@ -619,7 +653,7 @@ describe("HtmlElement", () => {
it("should return false if element does not match given selector", () => {
expect.assertions(3);
const node = root.querySelector("#spam");
const node = document.querySelector("#spam");
expect(node.matches("div > li")).toBeFalsy();
expect(node.matches("li.foo")).toBeFalsy();
expect(node.matches("#ham li")).toBeFalsy();
......@@ -629,14 +663,14 @@ describe("HtmlElement", () => {
describe("querySelector()", () => {
it("should find element by tagname", () => {
expect.assertions(2);
const el = root.querySelector("ul");
const el = document.querySelector("ul");
expect(el).toBeInstanceOf(HtmlElement);
expect(el.tagName).toEqual("ul");
});
it("should find element by #id", () => {
expect.assertions(3);
const el = root.querySelector("#parent");
const el = document.querySelector("#parent");
expect(el).toBeInstanceOf(HtmlElement);
expect(el.tagName).toEqual("div");
expect(el.getAttributeValue("id")).toEqual("parent");
......@@ -644,7 +678,7 @@ describe("HtmlElement", () => {
it("should find element by .class", () => {
expect.assertions(3);
const el = root.querySelector(".foo");
const el = document.querySelector(".foo");
expect(el).toBeInstanceOf(HtmlElement);
expect(el.tagName).toEqual("li");
expect(el.getAttributeValue("class")).toEqual("foo");
......@@ -652,7 +686,7 @@ describe("HtmlElement", () => {
it("should find element by [attr]", () => {
expect.assertions(3);
const el = root.querySelector("[title]");
const el = document.querySelector("[title]");
expect(el).toBeInstanceOf(HtmlElement);
expect(el.tagName).toEqual("li");
expect(el.getAttributeValue("class")).toEqual("bar baz");
......@@ -660,7 +694,7 @@ describe("HtmlElement", () => {
it('should find element by [attr=".."]', () => {
expect.assertions(3);
const el = root.querySelector('[class="foo"]');
const el = document.querySelector('[class="foo"]');
expect(el).toBeInstanceOf(HtmlElement);
expect(el.tagName).toEqual("li");
expect(el.getAttributeValue("class")).toEqual("foo");
......@@ -668,7 +702,7 @@ describe("HtmlElement", () => {
it("should find element with compound selector", () => {
expect.assertions(3);
const el = root.querySelector(".bar.baz#spam");
const el = document.querySelector(".bar.baz#spam");
expect(el).toBeInstanceOf(HtmlElement);
expect(el.tagName).toEqual("li");
expect(el.getAttributeValue("class")).toEqual("bar baz");
......@@ -676,7 +710,7 @@ describe("HtmlElement", () => {
it("should find element with descendant combinator", () => {
expect.assertions(3);
const el = root.querySelector("ul .bar");
const el = document.querySelector("ul .bar");
expect(el).toBeInstanceOf(HtmlElement);
expect(el.tagName).toEqual("li");
expect(el.getAttributeValue("class")).toEqual("bar baz");
......@@ -684,7 +718,7 @@ describe("HtmlElement", () => {
it("should find element with child combinator", () => {
expect.assertions(3);
const el = root.querySelector("div > .bar");
const el = document.querySelector("div > .bar");
expect(el).toBeInstanceOf(HtmlElement);
expect(el.tagName).toEqual("p");
expect(el.getAttributeValue("class")).toEqual("bar");
......@@ -692,7 +726,7 @@ describe("HtmlElement", () => {
it("should find element with multiple child combinators", () => {
expect.assertions(3);
const el = root.querySelector("#parent > ul > li");
const el = document.querySelector("#parent > ul > li");
expect(el).toBeInstanceOf(HtmlElement);
expect(el.tagName).toEqual("li");
expect(el.getAttributeValue("class")).toEqual("foo");
......@@ -700,7 +734,7 @@ describe("HtmlElement", () => {
it("should find element with adjacent sibling combinator", () => {
expect.assertions(3);
const el = root.querySelector("li + li");
const el = document.querySelector("li + li");
expect(el).toBeInstanceOf(HtmlElement);
expect(el.tagName).toEqual("li");
expect(el.getAttributeValue("class")).toEqual("bar baz");
......@@ -708,21 +742,38 @@ describe("HtmlElement", () => {
it("should find element with general sibling combinator", () => {
expect.assertions(3);
const el = root.querySelector("ul ~ .baz");
const el = document.querySelector("ul ~ .baz");
expect(el).toBeInstanceOf(HtmlElement);
expect(el.tagName).toEqual("span");
expect(el.getAttributeValue("class")).toEqual("baz");
});
it("should find element with :scope", () => {
expect.assertions(1);
const markup = `
<h1 id="first"></h1>
<section>
<div><h1 id="second"></h1></div>
<h1 id="third"></h1>
<div><h1 id="forth"></h1></div>
</section>
<h1 id="fifth"></h1>`;
const parser = new Parser(Config.empty().resolve());
const document = parser.parseHtml(markup);
const section = document.querySelector("section");
const el = section.querySelectorAll(":scope > h1");
expect(el.map((it) => it.id)).toEqual(["third"]);
});
it("should return null if nothing matches", () => {
expect.assertions(1);
const el = root.querySelector("foobar");
const el = document.querySelector("foobar");
expect(el).toBeNull();
});
it("should return null if selector is empty", () => {
expect.assertions(1);
const el = root.querySelector("");
const el = document.querySelector("");
expect(el).toBeNull();
});
});
......@@ -730,7 +781,7 @@ describe("HtmlElement", () => {
describe("querySelectorAll()", () => {
it("should find multiple elements", () => {
expect.assertions(5);
const el = root.querySelectorAll(".bar");
const el = document.querySelectorAll(".bar");
expect(el).toHaveLength(2);
expect(el[0]).toBeInstanceOf(HtmlElement);
expect(el[1]).toBeInstanceOf(HtmlElement);
......@@ -740,7 +791,7 @@ describe("HtmlElement", () => {
it("should handle multiple selectors", () => {
expect.assertions(4);
const el = root.querySelectorAll(".bar, li");
const el = document.querySelectorAll(".bar, li");
el.sort(
(a: HtmlElement, b: HtmlElement) => a.unique - b.unique
); /* selector may give results in any order */
......@@ -752,13 +803,13 @@ describe("HtmlElement", () => {
it("should return [] when nothing matches", () => {
expect.assertions(1);
const el = root.querySelectorAll("missing");
const el = document.querySelectorAll("missing");
expect(el).toEqual([]);
});
it("should return [] if selector is empty", () => {
expect.assertions(1);
const el = root.querySelectorAll("");
const el = document.querySelectorAll("");
expect(el).toEqual([]);
});
});
......@@ -865,6 +916,25 @@ describe("HtmlElement", () => {
expect(result?.tagName).toEqual("b");
});
});
it("should not throw error if tagName is undefined", () => {
expect.assertions(1);
expect(() => new HtmlElement(undefined, null, NodeClosed.EndTag, null, location)).not.toThrow();
});
it("should throw error if tagName is empty string", () => {
expect.assertions(1);
expect(() => new HtmlElement("", null, NodeClosed.EndTag, null, location)).toThrow(
"The tag name provided ('') is not a valid name"
);
});
it("should throw error if tagName is asterisk", () => {
expect.assertions(1);
expect(() => new HtmlElement("*", null, NodeClosed.EndTag, null, location)).toThrow(
"The tag name provided ('*') is not a valid name"
);
});
});
function mockEntry(stub: Partial<MetaData> = {}): MetaData {
......