add tests and graphic corrections

This commit is contained in:
poongaloo 2026-06-08 18:16:16 +02:00
parent 9ceba31662
commit f4cfeb954d
12 changed files with 2424 additions and 60 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
test-results/
playwright-report/

View File

@ -24,43 +24,61 @@
box-sizing: border-box;
}
.book-frame {
width: min(720px, calc(100vw - 56px));
aspect-ratio: 72 / 46;
}
#book {
width: 720px;
height: 460px;
width: 100%;
height: 100%;
background: #cbd3df;
}
#book .page {
display: flex;
flex-direction: column;
gap: 16px;
width: 360px;
height: 460px;
box-sizing: border-box;
padding: 28px;
border: 1px solid #aeb8c8;
background: #fff8df;
font-size: 18px;
line-height: 1.2;
overflow: hidden;
}
#book .page:nth-child(even) {
#book .page.alt {
background: #e7f1ff;
}
#book .page h1,
#book .page h2 {
#book .page-content {
display: flex;
flex-direction: column;
gap: 16px;
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 28px;
border: 1px solid #aeb8c8;
font-size: 18px;
line-height: 1.2;
overflow: hidden;
}
#book .page.alt .page-content {
background: #e7f1ff;
}
#book .page:not(.alt) .page-content {
background: #fff8df;
}
#book .page-content h1,
#book .page-content h2 {
margin: 0;
font-size: 30px;
line-height: 1.05;
}
#book .page p {
#book .page-content p {
margin: 0;
line-height: 1.45;
}
#book .page img {
#book .page-content img {
display: block;
width: 100%;
height: 180px;
@ -104,53 +122,76 @@
text-align: center;
}
@media (max-width: 820px) {
#book {
width: 92vw;
height: 58vw;
max-height: 460px;
@media (max-width: 620px) {
#book .page-content {
gap: 12px;
padding: 20px;
font-size: 15px;
}
#book .page-content h1,
#book .page-content h2 {
font-size: 24px;
}
#book .page-content img {
height: 130px;
}
}
</style>
</head>
<body>
<main>
<div id="book">
<div class="page">
<span class="kicker">Demo minimale</span>
<h1>Explorer turn.js</h1>
<img src="assets/cover.svg" alt="Livre ouvert avec pages colorees">
<p>Un exemple autonome pour verifier rapidement le chargement de jQuery 4, l'initialisation du plugin et le changement de page.</p>
</div>
<div class="page">
<span class="kicker">Page 2</span>
<h2>Un livre HTML</h2>
<p>Chaque page est un simple element HTML. Le plugin ajoute les wrappers, gere les vues et anime le passage d'une page a l'autre.</p>
<img src="assets/layout.svg" alt="Composition de pages dans un navigateur">
</div>
<div class="page">
<span class="kicker">Page 3</span>
<h2>Images et texte</h2>
<img src="assets/media.svg" alt="Image abstraite avec cadres de contenu">
<p>La demo contient du texte et des images locales afin de tester un rendu un peu plus proche d'un cas reel.</p>
</div>
<div class="page">
<span class="kicker">Page 4</span>
<h2>Navigation</h2>
<p>Utilise les boutons, les fleches du clavier ou le coin de page pour changer de vue.</p>
<img src="assets/navigation.svg" alt="Fleches de navigation autour d'un livre">
</div>
<div class="page">
<span class="kicker">Page 5</span>
<h2>Compatibilite</h2>
<img src="assets/browser.svg" alt="Fenetre de navigateur avec indicateurs de compatibilite">
<p>Cette page charge jQuery 4.0.0 depuis le CDN officiel et la version locale de turn.js depuis le depot clone.</p>
</div>
<div class="page">
<span class="kicker">Page 6</span>
<h2>Fin du test</h2>
<p>Si cette derniere page s'affiche et que le retour fonctionne, le scenario minimal est operationnel.</p>
<img src="assets/finish.svg" alt="Marque de validation sur une page">
<div class="book-frame">
<div id="book">
<div class="page">
<div class="page-content">
<span class="kicker">Demo minimale</span>
<h1>Explorer turn.js</h1>
<img src="assets/cover.svg" alt="Livre ouvert avec pages colorees">
<p>Un exemple autonome pour verifier rapidement le chargement de jQuery 4, l'initialisation du plugin et le changement de page.</p>
</div>
</div>
<div class="page alt">
<div class="page-content">
<span class="kicker">Page 2</span>
<h2>Un livre HTML</h2>
<p>Chaque page est un simple element HTML. Le plugin ajoute les wrappers, gere les vues et anime le passage d'une page a l'autre.</p>
<img src="assets/layout.svg" alt="Composition de pages dans un navigateur">
</div>
</div>
<div class="page">
<div class="page-content">
<span class="kicker">Page 3</span>
<h2>Images et texte</h2>
<img src="assets/media.svg" alt="Image abstraite avec cadres de contenu">
<p>La demo contient du texte et des images locales afin de tester un rendu un peu plus proche d'un cas reel.</p>
</div>
</div>
<div class="page alt">
<div class="page-content">
<span class="kicker">Page 4</span>
<h2>Navigation</h2>
<p>Utilise les boutons, les fleches du clavier ou le coin de page pour changer de vue.</p>
<img src="assets/navigation.svg" alt="Fleches de navigation autour d'un livre">
</div>
</div>
<div class="page">
<div class="page-content">
<span class="kicker">Page 5</span>
<h2>Compatibilite</h2>
<img src="assets/browser.svg" alt="Fenetre de navigateur avec indicateurs de compatibilite">
<p>Cette page charge jQuery 4.0.0 depuis le CDN officiel et la version locale de turn.js depuis le depot clone.</p>
</div>
</div>
<div class="page alt">
<div class="page-content">
<span class="kicker">Page 6</span>
<h2>Fin du test</h2>
<p>Si cette derniere page s'affiche et que le retour fonctionne, le scenario minimal est operationnel.</p>
<img src="assets/finish.svg" alt="Marque de validation sur une page">
</div>
</div>
</div>
</div>
@ -166,15 +207,34 @@
<script>
$(function() {
var $book = $('#book');
var $frame = $('.book-frame');
var $status = $('#status');
function currentBookSize() {
var width = Math.floor($frame.width());
if (width % 2) width -= 1;
return {
width: width,
height: Math.round(width * 460 / 720)
};
}
function syncBookSize() {
var size = currentBookSize();
$frame.css('height', size.height);
$book.turn('size', size.width, size.height);
}
function updateStatus() {
$status.text('Page ' + $book.turn('page') + ' / ' + $book.turn('pages'));
}
var initialSize = currentBookSize();
$frame.css('height', initialSize.height);
$book.turn({
width: 720,
height: 460,
width: initialSize.width,
height: initialSize.height,
display: 'double',
gradients: true,
acceleration: true,
@ -196,6 +256,8 @@
if (event.key === 'ArrowRight') $book.turn('next');
});
$(window).on('resize.turnDemo', syncBookSize);
updateStatus();
});
</script>

1962
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "turn.js",
"version": "3.0.0-local",
"private": true,
"type": "module",
"scripts": {
"test": "npm run test:unit && npm run test:visual",
"test:unit": "vitest run",
"test:visual": "playwright test",
"test:visual:update": "playwright test --update-snapshots"
},
"devDependencies": {
"@playwright/test": "1.60.0",
"jquery": "4.0.0",
"jsdom": "27.2.0",
"vitest": "^4.1.8"
}
}

21
playwright.config.mjs Normal file
View File

@ -0,0 +1,21 @@
import { defineConfig } from '@playwright/test';
import { existsSync } from 'node:fs';
const chromiumExecutable = process.env.CHROMIUM_EXECUTABLE_PATH || '/usr/bin/chromium';
export default defineConfig({
testDir: './tests/visual',
timeout: 30_000,
expect: {
timeout: 5_000,
toHaveScreenshot: {
maxDiffPixelRatio: 0.02
}
},
use: {
browserName: 'chromium',
viewport: { width: 1100, height: 760 },
deviceScaleFactor: 1,
launchOptions: existsSync(chromiumExecutable) ? { executablePath: chromiumExecutable } : {}
}
});

View File

@ -0,0 +1,143 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { JSDOM } from 'jsdom';
import { createRequire } from 'node:module';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
const require = createRequire(import.meta.url);
const __dirname = dirname(fileURLToPath(import.meta.url));
const jqueryDist = dirname(require.resolve('jquery'));
const jquerySource = readFileSync(resolve(jqueryDist, 'jquery.js'), 'utf8');
const turnSource = readFileSync(resolve(__dirname, '../../turn.js'), 'utf8');
function createFixture(pageCount = 4) {
const pages = Array.from({ length: pageCount }, (_, index) => (
`<div class="page">Page ${index + 1}</div>`
)).join('');
const dom = new JSDOM(`<!doctype html><html><body><div id="book">${pages}</div></body></html>`, {
pretendToBeVisual: true,
runScripts: 'outside-only',
url: 'http://localhost/'
});
dom.window.eval(jquerySource);
const $ = dom.window.jQuery;
dom.window.eval(turnSource);
return { window: dom.window, document: dom.window.document, $ };
}
describe('turn.js jQuery plugin', () => {
let fixture;
beforeEach(() => {
fixture = createFixture();
});
afterEach(() => {
fixture.window.close();
vi.restoreAllMocks();
});
it('registers the turn, flip and transform plugins', () => {
const { $ } = fixture;
expect($.fn.turn).toBeTypeOf('function');
expect($.fn.flip).toBeTypeOf('function');
expect($.fn.transform).toBeTypeOf('function');
});
it('initializes a book and exposes page, view, pages and size state', () => {
const { $ } = fixture;
const $book = $('#book');
$book.turn({
width: 600,
height: 400,
display: 'double',
gradients: false,
acceleration: false
});
expect($book.turn('page')).toBe(1);
expect($book.turn('pages')).toBe(4);
expect($book.turn('view')).toEqual([0, 1]);
expect($book.turn('size')).toEqual({ width: 600, height: 400 });
expect($book.find('.turn-page-wrapper').length).toBeGreaterThan(0);
});
it('supports initial page selection without animation', () => {
const { $ } = fixture;
const $book = $('#book');
$book.turn({
width: 600,
height: 400,
display: 'double',
gradients: false,
acceleration: false,
page: 2
});
expect($book.turn('page')).toBe(2);
expect($book.turn('view')).toEqual([2, 3]);
});
it('can add and remove pages dynamically', () => {
const { $ } = fixture;
const $book = $('#book');
$book.turn({
width: 600,
height: 400,
display: 'double',
gradients: false,
acceleration: false
});
$book.turn('addPage', $('<div class="page">Page 5</div>'), 5);
expect($book.turn('pages')).toBe(5);
expect($book.turn('hasPage', 5)).toBe(true);
$book.turn('removePage', 5);
expect($book.turn('pages')).toBe(4);
expect($book.turn('hasPage', 5)).toBe(false);
});
it('registers internal handlers with event namespaces', () => {
fixture.window.close();
fixture = createFixture();
const { $ } = fixture;
const onSpy = vi.spyOn($.fn, 'on');
$('#book').turn({
width: 600,
height: 400,
display: 'double',
gradients: false,
acceleration: false,
when: {
turned: function() {}
}
});
const eventNames = onSpy.mock.calls
.map(call => call[0])
.filter(name => typeof name === 'string');
const pointerEvents = $.isTouch
? ['touchstart.turn', 'touchmove.turn', 'touchend.turn']
: ['mousedown.turn', 'mousemove.turn', 'mouseup.turn'];
expect(eventNames).toContain('turned.turn');
expect(eventNames).toEqual(expect.arrayContaining(pointerEvents));
expect(eventNames).toContain('pressed.turnFlip');
expect(eventNames).toContain('released.turnFlip');
expect(eventNames).toContain('start.turnFlip');
expect(eventNames).toContain('end.turnFlip');
expect(eventNames).toContain('flip.turnFlip');
});
});

View File

@ -0,0 +1,148 @@
import { expect, test } from '@playwright/test';
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { extname, join, normalize } from 'node:path';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
const require = createRequire(import.meta.url);
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, '../..');
const jqueryDist = dirname(require.resolve('jquery'));
const jqueryPath = resolve(jqueryDist, 'jquery.min.js');
const contentTypes = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.svg': 'image/svg+xml; charset=utf-8'
};
let server;
let baseUrl;
test.beforeAll(async () => {
server = createServer(async (request, response) => {
try {
const url = new URL(request.url, 'http://127.0.0.1');
const pathname = url.pathname === '/' ? '/demos/minimal/index.html' : url.pathname;
const file = normalize(join(root, pathname));
if (!file.startsWith(root)) {
response.writeHead(403);
response.end('Forbidden');
return;
}
const body = await readFile(file);
response.writeHead(200, {
'content-type': contentTypes[extname(file)] || 'application/octet-stream'
});
response.end(body);
} catch (error) {
response.writeHead(404);
response.end(error.message);
}
});
await new Promise(resolveListen => server.listen(0, '127.0.0.1', resolveListen));
baseUrl = `http://127.0.0.1:${server.address().port}`;
});
test.afterAll(async () => {
await new Promise(resolveClose => server.close(resolveClose));
});
test.beforeEach(async ({ page }) => {
await page.route('https://code.jquery.com/jquery-4.0.0.min.js', route => {
route.fulfill({
path: jqueryPath,
contentType: 'text/javascript; charset=utf-8'
});
});
});
test('minimal demo renders the opening page', async ({ page }) => {
await page.goto(`${baseUrl}/demos/minimal/index.html`);
await page.waitForFunction(() => window.jQuery && window.jQuery.fn.jquery === '4.0.0' && typeof window.jQuery.fn.turn === 'function');
await expect(page.locator('#book')).toHaveScreenshot('minimal-book-page-1.png');
});
test('minimal demo renders after turning to the next page', async ({ page }) => {
await page.goto(`${baseUrl}/demos/minimal/index.html`);
await page.waitForFunction(() => window.jQuery && window.jQuery.fn.jquery === '4.0.0' && typeof window.jQuery.fn.turn === 'function');
await page.locator('#next').click();
await expect(page.locator('#status')).toHaveText('Page 2 / 6');
await page.waitForTimeout(700);
await expect(page.locator('#book')).toHaveScreenshot('minimal-book-page-2.png');
});
test('minimal demo keeps turn geometry aligned in a compact viewport', async ({ page }) => {
await page.setViewportSize({ width: 600, height: 720 });
await page.goto(`${baseUrl}/demos/minimal/index.html`);
await page.waitForFunction(() => window.jQuery && window.jQuery.fn.jquery === '4.0.0' && typeof window.jQuery.fn.turn === 'function');
const metrics = await page.evaluate(() => {
const book = document.querySelector('#book').getBoundingClientRect();
const wrapper = document.querySelector('.turn-page-wrapper').getBoundingClientRect();
const size = $('#book').turn('size');
return {
book: { width: Math.round(book.width), height: Math.round(book.height) },
wrapper: { width: Math.round(wrapper.width), height: Math.round(wrapper.height) },
size
};
});
expect(metrics.size).toEqual(metrics.book);
expect(metrics.wrapper.height).toBe(metrics.book.height);
expect(metrics.wrapper.width).toBe(metrics.book.width / 2);
await expect(page.locator('#book')).toHaveScreenshot('minimal-book-compact-page-1.png');
});
test('minimal demo keeps page measurements aligned during a page turn', async ({ page }) => {
await page.goto(`${baseUrl}/demos/minimal/index.html`);
await page.waitForFunction(() => window.jQuery && window.jQuery.fn.jquery === '4.0.0' && typeof window.jQuery.fn.turn === 'function');
await page.locator('#next').click();
await page.waitForTimeout(700);
await page.locator('#next').click();
await page.waitForTimeout(700);
await expect(page.locator('#status')).toHaveText('Page 4 / 6');
await page.locator('#next').click();
await page.waitForTimeout(160);
const metrics = await page.evaluate(() => {
const book = document.querySelector('#book').getBoundingClientRect();
const pageWidth = Math.round(book.width / 2);
const pageHeight = Math.round(book.height);
const pages = [...document.querySelectorAll('.turn-page')]
.map(element => {
const rect = element.getBoundingClientRect();
return {
page: element.className,
width: Math.round(rect.width),
height: Math.round(rect.height),
jqueryWidth: Math.round($(element).width()),
jqueryHeight: Math.round($(element).height())
};
})
.filter(item => item.width || item.height);
return { pageWidth, pageHeight, pages };
});
for (const item of metrics.pages) {
expect(item.width, item.page).toBe(metrics.pageWidth);
expect(item.height, item.page).toBe(metrics.pageHeight);
expect(item.jqueryWidth, item.page).toBe(metrics.pageWidth);
expect(item.jqueryHeight, item.page).toBe(metrics.pageHeight);
}
await expect(page.locator('#book')).toHaveScreenshot('minimal-book-turning-page-4.png');
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

7
vitest.config.mjs Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['tests/unit/**/*.test.js']
}
});