Skip to content

Commit

Permalink
HTML snapshot test for all samples for consistency in the markup
Browse files Browse the repository at this point in the history
  • Loading branch information
divdavem committed Jul 27, 2023
1 parent ecfeeb7 commit e30e4b4
Show file tree
Hide file tree
Showing 43 changed files with 2,139 additions and 64 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ coverage/
playwright-report/
test-results/
.svelte-kit/
*-snapshots/
4 changes: 2 additions & 2 deletions angular/demo/src/app/samples/alert/config.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ export enum AlertStatus {
@Component({
standalone: true,
imports: [AgnosUIAngularModule, NgFor, FormsModule],
template: ` <button class="btn btn-primary showAlert" (click)="showAlert(alert)">Show alert</button>
template: ` <button class="btn btn-primary showAlert" (click)="showAlert(alert)" type="button">Show alert</button>
<br />
<br />
<div class="d-flex flex-column">
<div class="d-flex form-group">
<label class="align-self-center me-3" for="typeSelect">Alert type: </label>
<select id="typeSelect" class="form-select w-auto" [(ngModel)]="type">
<option *ngFor="let style of styleList" [ngValue]="style.value">{{ style.label }}</option>
<option *ngFor="let style of styleList" [value]="style.value">{{ style.label }}</option>
</select>
</div>
Expand Down
22 changes: 10 additions & 12 deletions angular/demo/src/app/samples/alert/dynamic.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,16 @@ class AlertContainerService {
standalone: true,
imports: [AlertComponent, NgFor],
template: `
<div #errorArea>
<au-alert
*ngFor="let alert of alertContainerService.alerts"
[animation]="alert.animation"
[animationOnInit]="alert.animationOnInit"
[dismissible]="alert.dismissible"
[type]="alert.type"
[slotDefault]="alert.slotDefault"
(hidden)="removeAlert(alert)"
>
</au-alert>
</div>
<au-alert
*ngFor="let alert of alertContainerService.alerts"
[animation]="alert.animation"
[animationOnInit]="alert.animationOnInit"
[dismissible]="alert.dismissible"
[type]="alert.type"
[slotDefault]="alert.slotDefault"
(hidden)="removeAlert(alert)"
>
</au-alert>
`,
})
class ChildComponent {
Expand Down
2 changes: 1 addition & 1 deletion angular/demo/src/app/samples/alert/icon.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default class IconAlertComponent {
providers: [SlotDirective],
template: `
<ng-template #iconDemo let-state="state" let-widget="widget">
<span class="d-flex align-items-center me-1" [innerHTML]="sanitizer.bypassSecurityTrustHtml(typeIcon[state.type])"> </span>
<span class="d-flex align-items-center svg icon-20 me-1" [innerHTML]="sanitizer.bypassSecurityTrustHtml(typeIcon[state.type])"> </span>
<div class="d-flex w-100 alert-body">
<ng-template [auSlot]="state.slotDefault" [auSlotProps]="{widget, state}"></ng-template>
<button
Expand Down
2 changes: 1 addition & 1 deletion angular/demo/src/app/samples/modal/default.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {FormsModule} from '@angular/forms';
standalone: true,
imports: [AgnosUIAngularModule, CommonModule, FormsModule],
template: `
<button class="btn btn-primary" (click)="show(modal)">Launch demo modal</button>
<button class="btn btn-primary" type="button" (click)="show(modal)">Launch demo modal</button>
<div class="mt-3" data-testid="message">{{ message }}</div>
<au-modal #modal slotTitle="Save changes">
Do you want to save your changes?
Expand Down
8 changes: 4 additions & 4 deletions angular/demo/src/app/samples/pagination/config.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const FILTER_PAG_REGEX = /[^0-9]/g;
*ngIf="!widget.api.isEllipsis(page)"
[attr.aria-label]="state.pagesLabel[i]"
class="page-link au-page"
href
href="#"
(click)="widget.actions.select(page); $event.preventDefault()"
[attr.tabindex]="state.disabled ? '-1' : null"
[attr.aria-disabled]="state.disabled ? 'true' : null"
Expand Down Expand Up @@ -77,7 +77,7 @@ const FILTER_PAG_REGEX = /[^0-9]/g;
</button>
</div>
<br />
Collection Size:
Collection size:
<div id="btn-config-collectionSize" class="btn-group mb-2">
<button
class="btn btn-sm btn-outline-secondary"
Expand All @@ -102,7 +102,7 @@ const FILTER_PAG_REGEX = /[^0-9]/g;
</button>
<button
class="btn btn-sm btn-outline-secondary"
[class.active]="(widgetsConfig$ | async)?.pagination?.collectionSize === undefined"
[class.active]="!(widgetsConfig$ | async)?.pagination?.collectionSize"
(click)="updatePaginationConfig({collectionSize: undefined})"
>
undefined
Expand Down Expand Up @@ -148,7 +148,7 @@ const FILTER_PAG_REGEX = /[^0-9]/g;
[class.active]="(widgetsConfig$ | async)?.pagination?.slotPages === custom"
(click)="updatePaginationConfig({slotPages: custom})"
>
custom Pages
custom pages
</button>
<button
class="btn btn-sm btn-outline-secondary"
Expand Down
2 changes: 1 addition & 1 deletion angular/demo/src/app/samples/rating/config.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import {Component} from '@angular/core';
</button>
<button
class="btn btn-sm btn-outline-secondary"
[class.active]="(widgetsConfig$ | async)?.rating?.maxRating === undefined"
[class.active]="!(widgetsConfig$ | async)?.rating?.maxRating"
(click)="updateRatingConfig({maxRating: undefined})"
>
undefined
Expand Down
4 changes: 2 additions & 2 deletions angular/demo/src/app/samples/select/select.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {FormsModule} from '@angular/forms';
template: `
<h2>Multiselect example</h2>
<div class="mb-3">
<label for="select-example" class="form-label">Multiselect</label>
<au-select id="select-example" [items]="items" [(filterText)]="filterText"></au-select>
<label class="form-label">Multiselect</label>
<au-select [items]="items" [(filterText)]="filterText"></au-select>
</div>
<div class="demo-select-config">
<strong>Default config</strong><br />
Expand Down
2 changes: 1 addition & 1 deletion angular/demo/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
<app-root id="root"></app-root>
</body>
</html>
7 changes: 6 additions & 1 deletion angular/lib/src/lib/alert/alert.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,12 @@ const defaultConfig: Partial<AlertProps> = {
<ng-content></ng-content>
</ng-template>
<div *ngIf="!state().hidden" [auUse]="widget.directives.transitionDirective" class="d-flex w-100 alert alert-{{ state().type }}" role="alert">
<div
*ngIf="!state().hidden"
[auUse]="widget.directives.transitionDirective"
class="au-alert d-flex w-100 alert alert-{{ state().type }}"
role="alert"
>
<ng-template [auSlot]="state().slotStructure" [auSlotProps]="{state: state(), widget}"></ng-template>
</div>`,
})
Expand Down
11 changes: 6 additions & 5 deletions angular/lib/src/lib/pagination/pagination.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class PaginationPagesDirective {
*ngIf="!widget.api.isEllipsis(page)"
[attr.aria-label]="state.pagesLabel[i]"
class="page-link au-page"
href
href="#"
(click)="widget.actions.select(page); $event.preventDefault()"
[attr.tabindex]="state.disabled ? '-1' : null"
[attr.aria-disabled]="state.disabled ? 'true' : null"
Expand Down Expand Up @@ -176,7 +176,7 @@ const defaultConfig: Partial<PaginationProps> = {
<a
[attr.aria-label]="state.ariaFirstLabel"
class="page-link au-first"
href
href="#"
(click)="widget.actions.first(); $event.preventDefault()"
[attr.tabindex]="state.previousDisabled ? '-1' : null"
[attr.aria-disabled]="state.previousDisabled ? 'true' : null"
Expand All @@ -190,7 +190,7 @@ const defaultConfig: Partial<PaginationProps> = {
<a
[attr.aria-label]="state.ariaPreviousLabel"
class="page-link au-previous"
href
href="#"
(click)="widget.actions.previous(); $event.preventDefault()"
[attr.tabindex]="state.previousDisabled ? '-1' : null"
[attr.aria-disabled]="state.previousDisabled ? 'true' : null"
Expand All @@ -205,7 +205,7 @@ const defaultConfig: Partial<PaginationProps> = {
<a
[attr.aria-label]="state.ariaNextLabel"
class="page-link au-next"
href
href="#"
(click)="widget.actions.next(); $event.preventDefault()"
[attr.tabindex]="state.nextDisabled ? '-1' : null"
[attr.aria-disabled]="state.nextDisabled ? 'true' : null"
Expand All @@ -219,7 +219,7 @@ const defaultConfig: Partial<PaginationProps> = {
<a
[attr.aria-label]="state.ariaLastLabel"
class="page-link au-last"
href
href="#"
(click)="widget.actions.last(); $event.preventDefault()"
[attr.tabindex]="state.nextDisabled ? '-1' : null"
[attr.aria-disabled]="state.nextDisabled ? 'true' : null"
Expand All @@ -230,6 +230,7 @@ const defaultConfig: Partial<PaginationProps> = {
</a>
</li>
</ul>
<div aria-live="polite" class="visually-hidden">Current page is {{ state.page }}</div>
</ng-container>
`,
})
Expand Down
168 changes: 168 additions & 0 deletions e2e/htmlSnapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import type {Locator} from '@playwright/test';

export type HTMLAttribute = {name: string; value: string};
export type HTMLNode =
| null
| string
| {
tagName: string;
childNodes: HTMLNode[];
attributes: HTMLAttribute[];
};

export const htmlStructure = (locator: Locator): Promise<HTMLNode> =>
locator.evaluate((element) => {
const isElement = (node: Node): node is Element => node.nodeType === node.ELEMENT_NODE;
const recFn = (node: Node) => {
if (!isElement(node)) {
if (node.nodeType === node.TEXT_NODE) {
return node.nodeValue;
}
// Note: ignore comments
return null;
}
const tagName = node.tagName.toLowerCase();
const childNodes: any[] = [];
for (const child of node.childNodes) {
const value = recFn(child);
if (value != null) {
childNodes.push(value);
}
}
const attributes: HTMLAttribute[] = [];
for (const {name, value} of node.attributes) {
attributes.push({name, value});
}
return {tagName, childNodes, attributes};
};
return recFn(element);
});

const cleanChildNodes = (childNodes: HTMLNode[]) => {
childNodes = [...childNodes];
const result: HTMLNode[] = [];
for (let i = 0; i < childNodes.length; i++) {
let newValue = childNodes[i];
if (!newValue) continue;
if (typeof newValue === 'string') {
const previousValue = result[result.length - 1];
if (typeof previousValue === 'string') {
result.pop();
newValue = previousValue + newValue;
}
} else if (newValue.tagName === '') {
// only keep descendants
childNodes.splice(i, 1, ...newValue.childNodes);
i--;
continue;
}
result.push(newValue);
}
return removeWhiteSpace(result);
};

const removeWhiteSpace = (childNodes: HTMLNode[]) =>
childNodes.map((item) => (typeof item === 'string' ? item.replace(/\s+/g, ' ').trim() : item)).filter((item) => !!item);

const compare = (a: number | string, b: number | string) => (a < b ? -1 : a > b ? 1 : 0);
const compareName = ({name: a}: {name: string}, {name: b}: {name: string}) => compare(a, b);

const spaceRegExp = /\s+/;
const excludeClassRegExp = /^(s|ng)-/;
const excludeAttrRegExp = /^(ng-|_ng|slot$)/;

const excludeAttrSet = new Set([
'slot', // slot shouldn't be kept in the DOM by svelte, cf https://github.com/sveltejs/svelte/issues/8621

// The following attributes are used in our Angular components:
// FIXME (to discuss): do not use anymore the host element in AgnosUI Angular components
// so that we can filter it out (as it is done in au-alert, au-modal, and au-pagination in tagReplacements)
// and then remove the following list of attributes
'arialabel', // note: this should not be confused with the standard Aria attribute aria-label (which should not be removed)
'arialabelledby', // note: this should not be confused with the standard Aria attribute aria-labelledby (which should not be removed)
'classname',
]);

const removeTagsAndDescendants = new Set(['script', 'router-outlet']);
const tagReplacements = new Map([
['app-root', 'div'],
['au-alert', ''],
['au-modal', ''],
['au-pagination', ''],
['au-rating', 'div'],
['au-select', 'div'],
['ng-component', ''],
]);
const filterTagName = (tagName: string) => {
const mapResult = tagReplacements.get(tagName);
if (mapResult != null) {
return mapResult;
}
if (tagName.startsWith('app-')) {
return '';
}
return tagName;
};

export const filterHtmlStructure = (node: HTMLNode): HTMLNode => {
// only keep what we want to compare
if (!node || typeof node === 'string') {
return node;
}
let {tagName, attributes, childNodes} = node;
if (removeTagsAndDescendants.has(tagName)) {
return null;
}
tagName = filterTagName(tagName);
if (tagName == '') {
attributes = [];
}
attributes = attributes
.filter(({name, value}) => !(excludeAttrSet.has(name) || excludeAttrRegExp.test(name)))
.map(({name, value}) => {
if (name === 'class') {
value = value
.trim()
.split(spaceRegExp)
.filter((className) => !excludeClassRegExp.test(className))
.sort()
.join(' ');
}
return {name, value};
})
.sort(compareName);
childNodes = cleanChildNodes(childNodes.map(filterHtmlStructure));
return {
tagName,
attributes,
childNodes,
};
};

export const htmlSnapshot = async (locator: Locator) => {
const res: string[] = [];
const recFn = (node: HTMLNode, level = '') => {
if (node && typeof node === 'object') {
const {tagName, attributes, childNodes} = node;
const hasAttributes = attributes.length > 0;
const hasChildNodes = childNodes.length > 0;
res.push(`${level}<${tagName}${hasAttributes ? '' : hasChildNodes ? '>' : ' />'}`);
if (hasAttributes) {
for (const {name, value} of attributes) {
res.push(`${level} ${name}=${JSON.stringify(value)}`);
}
res.push(`${level}${hasChildNodes ? '>' : '/>'}`);
}
if (hasChildNodes) {
for (const child of childNodes) {
recFn(child, `${level}\t`);
}
res.push(`${level}</${tagName}>`);
}
} else {
res.push(level + JSON.stringify(node));
}
};
recFn(filterHtmlStructure(await htmlStructure(locator)));
return res.join('\n');
};
20 changes: 20 additions & 0 deletions e2e/samplesMarkup.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {test, expect} from '@playwright/test';
import {htmlSnapshot} from './htmlSnapshot';
import {globSync} from 'glob';
import path from 'path';

test.describe.parallel(`Samples markup consistency check`, () => {
const allRoutes = globSync('**/*.route.svelte', {cwd: path.join(__dirname, '../svelte/demo/samples')}).map(
(route) => `${route.replace(/\.route\.svelte$/, '').toLowerCase()}`
);
for (const route of allRoutes) {
test(`${route} should have a consistent markup`, async ({page}) => {
test.skip(route === 'pagination/pagination', 'FIXME: sample to be made consistent');
test.skip(route === 'focustrack/focustrack', 'FIXME: sample to be made consistent');
test.skip(route === 'select/select', 'FIXME: sample to be made consistent');
await page.goto(`#/${route}`);
await page.waitForSelector('.fade', {state: 'detached'}); // wait for fade transitions to be finished
expect(await htmlSnapshot(page.locator('body'))).toMatchSnapshot(`${route}.html`);
});
}
});
Loading

0 comments on commit e30e4b4

Please sign in to comment.