Skip to content

Commit

Permalink
Merge pull request #33 from swup/chore/tests
Browse files Browse the repository at this point in the history
Add tests
  • Loading branch information
daun committed Apr 17, 2024
2 parents e2e15af + 13823d1 commit 9407c3d
Show file tree
Hide file tree
Showing 10 changed files with 2,820 additions and 832 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Unit tests

on:
push:
branches: [main, master, next]
pull_request:
workflow_dispatch:

jobs:
run-tests:
name: Run unit tests
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Check out repo
uses: actions/checkout@v3

- name: Set up node
uses: actions/setup-node@v3
with:
node-version: 18

- run: npm ci
- run: npm run build

- name: Run tests
run: npm run test:unit
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ wiki-wishlist
*.sublime-workspace
.editorconfig
.idea
dist
/plugins
*.tgz
/dist
/tests/fixtures/dist
/tests/reports
/tests/results
3,427 changes: 2,621 additions & 806 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,22 @@
"dev": "swup package:dev",
"lint": "swup package:lint",
"format": "swup package:format",
"prepublishOnly": "npm run build"
"prepublishOnly": "npm run build",
"test": "npm run test:unit",
"test:unit": "vitest run --config ./tests/config/vitest.config.ts",
"test:unit:watch": "vitest --config ./tests/config/vitest.config.ts"
},
"author": {
"name": "Georgy Marchuk",
"email": "gmarcuk@gmail.com",
"url": "https://gmrchk.com/"
},
"contributors": [
{
"name": "Philipp Daun",
"email": "daun@daun.ltd",
"url": "https://philippdaun.net"
},
{
"name": "Rasso Hilber",
"email": "mail@rassohilber.com",
Expand All @@ -47,7 +55,10 @@
"@swup/plugin": "^4.0.0"
},
"devDependencies": {
"@swup/cli": "^5.0.0"
"@swup/cli": "^5.0.1",
"@types/jsdom": "^21.1.4",
"jsdom": "^24.0.0",
"vitest": "^1.2.2"
},
"peerDependencies": {
"swup": "^4.6.0"
Expand Down
10 changes: 10 additions & 0 deletions src/classes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function updateClassNames(el: HTMLElement, newEl: HTMLElement, { prefix = '' } = {}): void {
const remove = [...el.classList].filter((className) => isValid(className, { prefix }));
const add = [...newEl.classList].filter((className) => isValid(className, { prefix }));
el.classList.remove(...remove);
el.classList.add(...add);
}

function isValid(className: string, { prefix = '' } = {}): boolean {
return !!className && className.startsWith(prefix);
}
15 changes: 3 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Handler } from 'swup';
import Plugin from '@swup/plugin';
import { updateClassNames } from './classes.js';

type Options = {
/** If set, only classes beginning with this string will be added/removed. */
Expand All @@ -26,17 +27,7 @@ export default class SwupBodyClassPlugin extends Plugin {
}

protected updateBodyClass: Handler<'content:replace'> = (visit) => {
this.updateClassNames(document.body, visit.to.document!.body);
const { prefix } = this.options;
updateClassNames(document.body, visit.to.document!.body, { prefix });
};

protected updateClassNames(el: HTMLElement, newEl: HTMLElement) {
const remove = [...el.classList].filter((className) => this.isValidClassName(className));
const add = [...newEl.classList].filter((className) => this.isValidClassName(className));
el.classList.remove(...remove);
el.classList.add(...add);
}

protected isValidClassName(className: string) {
return className && className.startsWith(this.options.prefix);
}
}
15 changes: 15 additions & 0 deletions tests/config/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Vitest config file
* @see https://vitest.dev/config/
*/

import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
environment: 'jsdom',
include: ['tests/unit/**/*.test.ts'],
setupFiles: [],
testTimeout: 1000
}
});
65 changes: 65 additions & 0 deletions tests/unit/classes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest';
import { updateClassNames } from '../../src/classes.js';

const createElement = (className: string = '', { tagName = 'div' } = {}) => {
const el = document.createElement(tagName);
el.className = className;
return el;
};

const mergeClasses = (
currentClassName: string,
incomingClassName: string,
options: Parameters<typeof updateClassNames>[2] = {}
) => {
const current = createElement(currentClassName);
const incoming = createElement(incomingClassName);
updateClassNames(current, incoming, options);
return current;
};

describe('updateClassNames', () => {
describe('class names', () => {
it('adds a single class', () => {
const el = mergeClasses('', 'a');
expect(el.className).toBe('a');
});

it('removes a single class', () => {
const el = mergeClasses('a', '');
expect(el.className).toBe('');
});

it('clears out classname', () => {
const el = mergeClasses('a b c', '');
expect(el.className).toBe('');
});

it('builds up classname', () => {
const el = mergeClasses('', 'a b c');
expect(el.className).toBe('a b c');
});

it('only keeps classes present in new element', () => {
const el = mergeClasses('a b c', 'd');
expect(el.className).toBe('d');
});

it('keeps classes present in both elements', () => {
const el = mergeClasses('a b c', 'b e');
expect(el.className).toBe('b e');
});
});

describe('filtering', () => {
it('only adds classes matching the prefix', () => {
const el = mergeClasses('a b c', 'd pre-e f', { prefix: 'pre-' });
expect(el.className).toBe('a b c pre-e');
});

it('only removes classes matching the prefix', () => {
const el = mergeClasses('a b pre-c', 'd e', { prefix: 'pre-' });
expect(el.className).toBe('a b');
});
});
});
50 changes: 50 additions & 0 deletions tests/unit/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { vitest, describe, expect, it, beforeEach, afterEach } from 'vitest';
import Swup, { Visit } from 'swup';

import SwupBodyClassPlugin from '../../src/index.js';

vitest.mock('../../src/classes.js');

const page = { page: { html: '', url: '/' } };

describe('SwupBodyClassPlugin', () => {
let swup: Swup;
let plugin: SwupBodyClassPlugin;
let visit: Visit;

beforeEach(() => {
swup = new Swup();
plugin = new SwupBodyClassPlugin();
swup.use(plugin);
// @ts-ignore - createVisit is marked internal
visit = swup.createVisit({ url: '/' });
visit.to.document = new window.DOMParser().parseFromString(
'<html><head></head><body></body></html>',
'text/html'
);
});

afterEach(() => {
swup.unuse(plugin);
swup.destroy();
});

it('merges user options', async () => {
const plugin = new SwupBodyClassPlugin({ prefix: 'pre-' });
expect(plugin.options).toMatchObject({ prefix: 'pre-' });
});

it('updates body classname from content:replace hook handler', async () => {
const classes = await import('../../src/classes.js');
const spy = vitest.spyOn(classes, 'updateClassNames');
// classes.updateClassNames = vitest.fn()
// .mockImplementation(() => ({ removed: [], added: [] }));

await swup.hooks.call('content:replace', visit, page);

expect(spy).toHaveBeenCalledOnce();
expect(spy).toHaveBeenCalledWith(document.body, visit.to.document!.body, {
prefix: plugin.options.prefix
});
});
});
20 changes: 11 additions & 9 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
{
"include": ["src"],
"compilerOptions": {
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"moduleResolution": "Node16", /* Specify how TypeScript looks up a file from a given module specifier. */
"rootDirs": ["./src"], /* Allow multiple folders to be treated as one when resolving modules. */
"resolveJsonModule": true, /* Enable importing .json files. */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */
"target": "es2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDirs": ["./src"],
"resolveJsonModule": true,
"allowJs": true,
"outDir": "./dist",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noImplicitAny": true
}
}

0 comments on commit 9407c3d

Please sign in to comment.