Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/Views/frontend/book-detail.php
Original file line number Diff line number Diff line change
Expand Up @@ -2653,6 +2653,10 @@ function setFavUI(isFav) {

const baseOpts = {
dateFormat: 'Y-m-d',
// Force flatpickr's own calendar on mobile too: the native
// Android/iOS date picker ignores `disable`, so it would let the
// user pick fully-booked days and never show availability.
disableMobile: true,
altInput: true,
altFormat: forceEn ? 'm-d-Y' : 'd-m-Y',
minDate: 'today',
Expand Down
103 changes: 103 additions & 0 deletions tests/issue-mobile-datepicker.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// @ts-check
// Regression: on the public book page, the loan/reservation date picker must use
// Flatpickr's OWN calendar even on mobile (disableMobile:true). Without it,
// flatpickr falls back to the native Android/iOS date picker, which ignores the
// `disable` option — so fully-booked days look selectable and availability is
// never shown. Runs under Pixel 5 (Android Chrome) emulation (mobile user-agent + touch).
const { test, expect, devices } = require('@playwright/test');
const { execFileSync } = require('child_process');

const BASE = process.env.E2E_BASE_URL || 'http://localhost:8081';
const ADMIN_EMAIL = process.env.E2E_ADMIN_EMAIL || '';
const ADMIN_PASS = process.env.E2E_ADMIN_PASS || '';

const DB_USER = process.env.E2E_DB_USER || '';
const DB_PASS = process.env.E2E_DB_PASS || '';
const DB_HOST = process.env.E2E_DB_HOST || '';
const DB_PORT = process.env.E2E_DB_PORT || '';
const DB_SOCKET = process.env.E2E_DB_SOCKET || '';
const DB_NAME = process.env.E2E_DB_NAME || '';

test.skip(
!ADMIN_EMAIL || !ADMIN_PASS || !DB_USER || !DB_NAME,
'E2E credentials not configured (set E2E_ADMIN_EMAIL, E2E_ADMIN_PASS, E2E_DB_USER, E2E_DB_NAME)',
);

function dbQuery(sql) {
const args = [];
if (DB_HOST) {
args.push('-h', DB_HOST);
if (DB_PORT) args.push('-P', DB_PORT);
} else if (DB_SOCKET) {
args.push('-S', DB_SOCKET);
}
args.push('-u', DB_USER, DB_NAME, '-N', '-B', '-e', sql);
return execFileSync('mysql', args, {
encoding: 'utf-8', timeout: 10000,
env: { ...process.env, MYSQL_PWD: DB_PASS },
}).trim();
}

// Emulate a real phone (mobile UA + touch) — this is what makes flatpickr
// consider falling back to the native picker.
test.use({ ...devices['Pixel 5'] });

test.describe('Mobile: book-page loan date picker', () => {
let bookId = 0;

test.beforeAll(() => {
// A book with at least one available copy → the request-loan button shows.
bookId = parseInt(
dbQuery(
"SELECT c.libro_id FROM copie c JOIN libri l ON l.id=c.libro_id " +
"WHERE c.stato='disponibile' AND l.deleted_at IS NULL LIMIT 1",
) || '0',
10,
);
});

test('uses flatpickr (not the native picker), so availability can be shown', async ({ page }) => {
test.skip(!bookId, 'No available book to open the loan date picker on');

// Login (the request-loan dialog is for authenticated users)
await page.goto(`${BASE}/accedi`);
await page.fill('input[name="email"]', ADMIN_EMAIL);
await page.fill('input[name="password"]', ADMIN_PASS);
await page.locator('button[type="submit"]').click();
await page.waitForURL(/(\/$|admin|profilo|account)/, { timeout: 15000 }).catch(() => {});

await page.goto(`${BASE}/libro/${bookId}`);
const btn = page.locator('#btn-request-loan');
test.skip((await btn.count()) === 0, 'This book has no request-loan button');
Comment on lines +63 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Non rendere questo spec “false green”.

Il catch(() => {}) su waitForURL e lo test.skip sul bottone permettono allo spec di passare senza verificare nulla se il login rompe, cambia il redirect o #btn-request-loan non compare. Qui dovrebbe fallire esplicitamente, altrimenti la regressione mobile resta scoperta.

Diff proposta
     await page.goto(`${BASE}/accedi`);
     await page.fill('input[name="email"]', ADMIN_EMAIL);
     await page.fill('input[name="password"]', ADMIN_PASS);
     await page.locator('button[type="submit"]').click();
-    await page.waitForURL(/(\/$|admin|profilo|account)/, { timeout: 15000 }).catch(() => {});
+    await page.waitForURL(/(\/$|admin|profilo|account)/, { timeout: 15000 });

     await page.goto(`${BASE}/libro/${bookId}`);
     const btn = page.locator('`#btn-request-loan`');
-    test.skip((await btn.count()) === 0, 'This book has no request-loan button');
+    await expect(btn).toBeVisible();

Based on learnings, negli E2E di questo repo il gating deve evitare skip silenziosi che nascondono il fatto che il flusso non è stato davvero eseguito.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await page.goto(`${BASE}/accedi`);
await page.fill('input[name="email"]', ADMIN_EMAIL);
await page.fill('input[name="password"]', ADMIN_PASS);
await page.locator('button[type="submit"]').click();
await page.waitForURL(/(\/$|admin|profilo|account)/, { timeout: 15000 }).catch(() => {});
await page.goto(`${BASE}/libro/${bookId}`);
const btn = page.locator('#btn-request-loan');
test.skip((await btn.count()) === 0, 'This book has no request-loan button');
await page.goto(`${BASE}/accedi`);
await page.fill('input[name="email"]', ADMIN_EMAIL);
await page.fill('input[name="password"]', ADMIN_PASS);
await page.locator('button[type="submit"]').click();
await page.waitForURL(/(\/$|admin|profilo|account)/, { timeout: 15000 });
await page.goto(`${BASE}/libro/${bookId}`);
const btn = page.locator('`#btn-request-loan`');
await expect(btn).toBeVisible();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/issue-mobile-datepicker.spec.js` around lines 63 - 71, The mobile
datepicker spec is too permissive because the login redirect is swallowed and
the loan-request button check can silently skip the test, creating a false
green. In tests/issue-mobile-datepicker.spec.js, update the login flow around
page.waitForURL and the `#btn-request-loan` lookup so failures surface explicitly
instead of being ignored or skipped; keep the test in the executed path and fail
when the expected redirect or button is missing, using the existing page.goto,
page.waitForURL, and btn locator flow to locate the fix.

Source: Learnings


await btn.click();
await page.waitForSelector('.swal2-popup', { timeout: 8000 });
await page.waitForFunction(
() => {
const el = document.querySelector('#swal-date-start');
return el && /** @type {any} */ (el)._flatpickr;
},
{ timeout: 8000 },
);

const info = await page.evaluate(() => {
const el = /** @type {any} */ (document.querySelector('#swal-date-start'));
const fp = el && el._flatpickr;
return {
hasFlatpickr: !!fp,
disableMobile: fp ? !!fp.config.disableMobile : null,
// flatpickr sets isMobile=true only when it actually renders the native
// input; with disableMobile:true it must stay false even on a phone.
usesNativePicker: fp ? !!fp.isMobile : null,
nativeDateInput: !!document.querySelector('.swal2-popup input[type="date"]'),
customCalendar: !!document.querySelector('.flatpickr-calendar'),
};
});

expect(info.hasFlatpickr).toBe(true);
expect(info.disableMobile).toBe(true);
expect(info.usesNativePicker).toBe(false); // custom calendar, not the OS one
expect(info.nativeDateInput).toBe(false); // flatpickr did not inject a native <input type=date>
expect(info.customCalendar).toBe(true); // flatpickr's own calendar is in the DOM
});
});
Loading