diff --git a/javascript/dwa-starter-vanillajs-vite/index.html b/javascript/dwa-starter-vanillajs-vite/index.html index ba1ea68f..312cec62 100644 --- a/javascript/dwa-starter-vanillajs-vite/index.html +++ b/javascript/dwa-starter-vanillajs-vite/index.html @@ -4,7 +4,15 @@ DWA Starter + + + + + +
diff --git a/javascript/dwa-starter-vanillajs-vite/main.js b/javascript/dwa-starter-vanillajs-vite/main.js index 16c6f756..ad90c25d 100644 --- a/javascript/dwa-starter-vanillajs-vite/main.js +++ b/javascript/dwa-starter-vanillajs-vite/main.js @@ -1,101 +1,103 @@ +// main.js + +import { Home, About, Settings, NotFound } from "./components.js"; // Function to create and render the toggle button function createThemeToggleButton() { - console.log('Creating theme toggle button'); - const nav = document.querySelector('nav'); - const button = document.createElement('button'); - button.id = 'theme-toggle'; - button.textContent = 'Toggle Theme'; - button.setAttribute('aria-label', 'Toggle Dark Mode'); - button.classList.add('theme-toggle-btn'); - nav.appendChild(button); - button.addEventListener('click', toggleTheme); - console.log('Theme toggle button created and added to nav'); + console.log("Creating theme toggle button"); + const nav = document.querySelector("nav"); + const button = document.createElement("button"); + button.id = "theme-toggle"; + button.textContent = "Toggle Theme"; + button.setAttribute("aria-label", "Toggle Dark Mode"); + button.classList.add("theme-toggle-btn"); + nav.appendChild(button); + button.addEventListener("click", toggleTheme); + console.log("Theme toggle button created and added to nav"); } function toggleTheme() { - console.log('Toggle theme function called'); - const body = document.body; - const isDarkMode = body.classList.contains('dark-mode'); - console.log('Current mode is dark:', isDarkMode); - - if (isDarkMode) { - body.classList.remove('dark-mode'); - body.classList.add('light-mode'); - console.log('Switched to light mode:', body.classList); // Log class list - } else { - body.classList.remove('light-mode'); - body.classList.add('dark-mode'); - console.log('Switched to dark mode:', body.classList); // Log class list - } - localStorage.setItem('theme', isDarkMode ? 'light' : 'dark'); -} + console.log("Toggle theme function called"); + const body = document.body; + const isDarkMode = body.classList.contains("dark-mode"); + console.log("Current mode is dark:", isDarkMode); + if (isDarkMode) { + body.classList.remove("dark-mode"); + body.classList.add("light-mode"); + console.log("Switched to light mode:", body.classList); // Log class list + } else { + body.classList.remove("light-mode"); + body.classList.add("dark-mode"); + console.log("Switched to dark mode:", body.classList); // Log class list + } + localStorage.setItem("theme", isDarkMode ? "light" : "dark"); +} // Apply stored theme preference or system preference on load function applyStoredTheme() { - console.log('Applying stored theme'); - const storedTheme = localStorage.getItem('theme'); - const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); - const body = document.body; + console.log("Applying stored theme"); + const storedTheme = localStorage.getItem("theme"); + const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)"); + const body = document.body; - console.log('Stored theme:', storedTheme); - console.log('System prefers dark scheme:', prefersDarkScheme.matches); + console.log("Stored theme:", storedTheme); + console.log("System prefers dark scheme:", prefersDarkScheme.matches); - if (storedTheme === 'dark' || (storedTheme === null && prefersDarkScheme.matches)) { - body.classList.add('dark-mode'); - console.log('Applied dark mode'); - } else { - body.classList.add('light-mode'); - console.log('Applied light mode'); - } + if ( + storedTheme === "dark" || + (storedTheme === null && prefersDarkScheme.matches) + ) { + body.classList.add("dark-mode"); + console.log("Applied dark mode"); + } else { + body.classList.add("light-mode"); + console.log("Applied light mode"); + } } // Initial setup on DOM content loaded -document.addEventListener('DOMContentLoaded', () => { - console.log('DOM content loaded'); - applyStoredTheme(); // Apply the stored theme or system preference - createThemeToggleButton(); // Create the theme toggle button and attach to nav - // Initial routing setup (if using navigation in your app) - router(); - console.log('Initial setup completed'); +document.addEventListener("DOMContentLoaded", () => { + console.log("DOM content loaded"); + applyStoredTheme(); // Apply the stored theme or system preference + createThemeToggleButton(); // Create the theme toggle button and attach to nav + // Initial routing setup (if using navigation in your app) + router(); + console.log("Initial setup completed"); }); // Import your components for routing (if necessary) -import { Home, About, Settings, NotFound } from './components.js'; +import { Home, About, Settings, NotFound } from "./components.js"; // Define routes and their corresponding components (if necessary) const routes = { - '/': Home, - '/about': About, - '/settings': Settings, + "/": Home, + "/about": About, + "/settings": Settings, }; // Function to handle navigation (if necessary) function navigateTo(url) { - console.log('Navigating to:', url); - history.pushState(null, null, url); - router(); + history.pushState(null, null, url); + router(); } // Router function to render components based on the current URL function router() { - console.log('Router function called'); - const path = window.location.pathname; - console.log('Current path:', path); - const route = routes[path] || NotFound; - route(); + const path = window.location.pathname; + const route = routes[path] || NotFound; + route(); } -// Event delegation for link clicks (if necessary) -document.addEventListener('click', (e) => { - if (e.target.matches('[data-link]')) { - console.log('Link clicked:', e.target.href); - e.preventDefault(); - navigateTo(e.target.href); - } +// Event delegation for link clicks +document.addEventListener("click", (e) => { + if (e.target.matches("[data-link]")) { + e.preventDefault(); + navigateTo(e.target.href); + } }); -// Listen to popstate event (back/forward navigation) (if necessary) -window.addEventListener('popstate', router); +// Listen to popstate event (back/forward navigation) +window.addEventListener("popstate", router); -console.log('Script loaded'); \ No newline at end of file +// Initial call to router to render the correct component on page load +document.addEventListener("DOMContentLoaded", router); diff --git a/javascript/dwa-starter-vanillajs-vite/manifest.json b/javascript/dwa-starter-vanillajs-vite/manifest.json new file mode 100644 index 00000000..a9fa1bfb --- /dev/null +++ b/javascript/dwa-starter-vanillajs-vite/manifest.json @@ -0,0 +1,25 @@ +{ + "name": "dwa-starter-vanillajs-vite", + "display": "standalone", + "scope": "/", + "start_url": "/index.html", + "theme_color": "#ffffff", + "background_color": "#ffffff", + "icons": [ + { + "purpose": "maskable", + "sizes": "512x512", + "src": "./public/maskable-icon-512x512.png", + "type": "image/png" + }, + { + "purpose": "any", + "sizes": "512x512", + "src": "./public/pwa-512x512.png", + "type": "image/png" + } + ], + "orientation": "any", + "dir": "auto", + "lang": "en-US" +} diff --git a/javascript/dwa-starter-vanillajs-vite/package.json b/javascript/dwa-starter-vanillajs-vite/package.json index 6d2bc431..7f36f063 100644 --- a/javascript/dwa-starter-vanillajs-vite/package.json +++ b/javascript/dwa-starter-vanillajs-vite/package.json @@ -16,9 +16,13 @@ "jsdom": "^25.0.1", "playwright": "^1.47.2", "postcss": "^8.4.47", + "serwist": "^9.0.9", "tailwindcss": "^3.4.13", "vite": "^5.4.1", "vitest": "^2.1.2" }, - "packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228" + "packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228", + "dependencies": { + "@serwist/next": "^9.0.9" + } } diff --git a/javascript/dwa-starter-vanillajs-vite/playwright.config.js b/javascript/dwa-starter-vanillajs-vite/playwright.config.js index 18188058..ab1c5fba 100644 --- a/javascript/dwa-starter-vanillajs-vite/playwright.config.js +++ b/javascript/dwa-starter-vanillajs-vite/playwright.config.js @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * Read environment variables from file. * https://github.com/motdotla/dotenv @@ -9,7 +9,7 @@ import { defineConfig, devices } from '@playwright/test'; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './tests', + testDir: "./tests", /* Maximum time one test can run for. */ timeout: 30 * 1000, expect: { @@ -17,7 +17,7 @@ export default defineConfig({ * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ - timeout: 5000 + timeout: 5000, }, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, @@ -26,41 +26,41 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ actionTimeout: 0, /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:5173', + baseURL: "http://localhost:5173", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: "on-first-retry", /* Only on CI systems run the tests headless */ - headless: !!process.env.CI + headless: !!process.env.CI, }, /* Configure projects for major browsers */ projects: [ { - name: 'chromium', + name: "chromium", use: { - ...devices['Desktop Chrome'] - } + ...devices["Desktop Chrome"], + }, }, { - name: 'firefox', + name: "firefox", use: { - ...devices['Desktop Firefox'] - } + ...devices["Desktop Firefox"], + }, }, { - name: 'webkit', + name: "webkit", use: { - ...devices['Desktop Safari'] - } - } + ...devices["Desktop Safari"], + }, + }, /* Test against mobile viewports. */ // { @@ -101,9 +101,9 @@ export default defineConfig({ * Use the preview server on CI for more realistic testing. * Playwright will re-use the local server if there is already a dev-server running. */ - command: 'pnpm run dev', - url: 'http://localhost:5173', // Adjust this if your dev server uses a different port + command: "npm run dev", + url: "http://localhost:5173", // Adjust this if your dev server uses a different port reuseExistingServer: !process.env.CI, timeout: 120 * 1000, // 120 seconds - } -}) \ No newline at end of file + }, +}); diff --git a/javascript/dwa-starter-vanillajs-vite/public/apple-touch-icon-180x180.png b/javascript/dwa-starter-vanillajs-vite/public/apple-touch-icon-180x180.png new file mode 100644 index 00000000..9d7789fd Binary files /dev/null and b/javascript/dwa-starter-vanillajs-vite/public/apple-touch-icon-180x180.png differ diff --git a/javascript/dwa-starter-vanillajs-vite/public/favicon.ico b/javascript/dwa-starter-vanillajs-vite/public/favicon.ico new file mode 100644 index 00000000..07f419ee Binary files /dev/null and b/javascript/dwa-starter-vanillajs-vite/public/favicon.ico differ diff --git a/javascript/dwa-starter-vanillajs-vite/public/favicon.svg b/javascript/dwa-starter-vanillajs-vite/public/favicon.svg new file mode 100644 index 00000000..733f4fb4 --- /dev/null +++ b/javascript/dwa-starter-vanillajs-vite/public/favicon.svg @@ -0,0 +1,130 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/javascript/dwa-starter-vanillajs-vite/public/maskable-icon-512x512.png b/javascript/dwa-starter-vanillajs-vite/public/maskable-icon-512x512.png new file mode 100644 index 00000000..f9b949c2 Binary files /dev/null and b/javascript/dwa-starter-vanillajs-vite/public/maskable-icon-512x512.png differ diff --git a/javascript/dwa-starter-vanillajs-vite/public/pwa-192x192.png b/javascript/dwa-starter-vanillajs-vite/public/pwa-192x192.png new file mode 100644 index 00000000..b77fc500 Binary files /dev/null and b/javascript/dwa-starter-vanillajs-vite/public/pwa-192x192.png differ diff --git a/javascript/dwa-starter-vanillajs-vite/public/pwa-512x512.png b/javascript/dwa-starter-vanillajs-vite/public/pwa-512x512.png new file mode 100644 index 00000000..aeff8256 Binary files /dev/null and b/javascript/dwa-starter-vanillajs-vite/public/pwa-512x512.png differ diff --git a/javascript/dwa-starter-vanillajs-vite/public/pwa-64x64.png b/javascript/dwa-starter-vanillajs-vite/public/pwa-64x64.png new file mode 100644 index 00000000..287a1722 Binary files /dev/null and b/javascript/dwa-starter-vanillajs-vite/public/pwa-64x64.png differ diff --git a/javascript/dwa-starter-vanillajs-vite/sw.js b/javascript/dwa-starter-vanillajs-vite/sw.js new file mode 100644 index 00000000..7ca7fd37 --- /dev/null +++ b/javascript/dwa-starter-vanillajs-vite/sw.js @@ -0,0 +1,53 @@ +// Service Worker + +const cacheName = "dwa-starter-vanillajs-vite-v1"; // Name of the cache +const cacheAssets = ["/", "style.css", "main.js"]; // Assets to cache + +// Installation +self.addEventListener("install", (e) => { + console.log("Service Worker installed"); + + e.waitUntil( + caches + .open(cacheName) + .then((cache) => { + console.log("Service Worker: Caching files"); + cache.addAll(cacheAssets); + }) + .then(() => self.skipWaiting()) + ); +}); + +// Activation +self.addEventListener("activate", (e) => { + console.log("Service Worker activated"); + + e.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cache) => { + if (cache !== cacheName) { + console.log("Service Worker: Clearing old cache"); + return caches.delete(cache); + } + }) + ); + }) + ); + + // Claim control immediately after activation + return self.clients.claim(); +}); + +// Fetch event to serve cached assets when offline +self.addEventListener("fetch", (e) => { + console.log("Service Worker: Fetching"); + e.respondWith(fetch(e.request).catch(() => caches.match(e.request))); +}); + +// Listen for message from main.js to skip waiting and activate new SW immediately +self.addEventListener("message", (event) => { + if (event.data.action === "skipWaiting") { + self.skipWaiting(); + } +}); diff --git a/javascript/dwa-starter-vanillajs-vite/tests/addToHomeScreen.spec.js b/javascript/dwa-starter-vanillajs-vite/tests/addToHomeScreen.spec.js new file mode 100644 index 00000000..eafe8193 --- /dev/null +++ b/javascript/dwa-starter-vanillajs-vite/tests/addToHomeScreen.spec.js @@ -0,0 +1,29 @@ +import { test, expect } from "@playwright/test"; + +test("Add To Home Screen prompt should appear when eligible", async ({ + page, +}) => { + // Ensure the app is loaded + await page.goto("http://localhost:5173"); + + // Check if the manifest is available (this ensures the app is PWA-ready) + const manifest = await page.evaluate(() => { + return !!document.querySelector('link[rel="manifest"]'); + }); + expect(manifest).toBeTruthy(); // Make sure the manifest is present + + // Mock the beforeinstallprompt event + await page.evaluate(() => { + window.addEventListener("beforeinstallprompt", (e) => { + e.preventDefault(); // Prevents the default install prompt from appearing + window.deferredPrompt = e; // Save the event for later use + }); + // Dispatch the event manually + const event = new Event("beforeinstallprompt"); + window.dispatchEvent(event); + }); + + // Verify the mocked event was handled + const promptHandled = await page.evaluate(() => !!window.deferredPrompt); + expect(promptHandled).toBeTruthy(); +}); diff --git a/javascript/dwa-starter-vanillajs-vite/tests/main.spec.js b/javascript/dwa-starter-vanillajs-vite/tests/main.spec.js index 13858656..35bd46ad 100644 --- a/javascript/dwa-starter-vanillajs-vite/tests/main.spec.js +++ b/javascript/dwa-starter-vanillajs-vite/tests/main.spec.js @@ -1,70 +1,26 @@ // tests/main.spec.js -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; -test.describe('Vanilla Router with Theme Toggle', () => { - test.beforeEach(async ({ page }) => { - // Clear localStorage before each test to ensure isolation - await page.goto('/'); // Make sure you start from your app's root - await page.evaluate(() => localStorage.clear()); - }); - - test('should navigate to Home and toggle theme', async ({ page }) => { - await page.goto('/'); - - // Check the initial theme based on the system preference - const systemPrefersDark = await page.evaluate(() => window.matchMedia('(prefers-color-scheme: dark)').matches); - const appliedTheme = await page.evaluate(() => document.body.classList.contains('dark-mode')); - expect(appliedTheme).toBe(systemPrefersDark); - - // Click the theme toggle button - await page.click('#theme-toggle'); - - // Check if the theme has changed - const isDarkModeAfterToggle = await page.evaluate(() => document.body.classList.contains('dark-mode')); - expect(isDarkModeAfterToggle).toBe(!systemPrefersDark); - - // Verify localStorage has the correct theme - const storedTheme = await page.evaluate(() => localStorage.getItem('theme')); - expect(storedTheme).toBe(!systemPrefersDark ? 'dark' : 'light'); - }); +test.describe("Vanilla Router", () => { + // Before all tests, start a local server if necessary - test('should navigate to About', async ({ page }) => { - await page.goto('/about'); - expect(await page.textContent('h1')).toBe('DWA Starter Vanilla'); + test("should navigate to Home", async ({ page }) => { + await page.goto("/"); + expect(await page.textContent("h1")).toBe("Home"); }); - test('should navigate to Settings', async ({ page }) => { - await page.goto('/settings'); - expect(await page.textContent('h1')).toBe('Settings'); + test("should navigate to About", async ({ page }) => { + await page.goto("/about"); + expect(await page.textContent("h1")).toBe("DWA Starter Vanilla"); }); - test('should show Not Found for undefined routes', async ({ page }) => { - await page.goto('/undefined-route'); - expect(await page.textContent('h1')).toBe('404 - Page Not Found'); - }); - - test('should load correct theme based on localStorage', async ({ page }) => { - // Set theme to dark mode - await page.evaluate(() => localStorage.setItem('theme', 'dark')); - await page.goto('/'); - - const isDarkMode = await page.evaluate(() => document.body.classList.contains('dark-mode')); - expect(isDarkMode).toBe(true); - - // Set theme to light mode - await page.evaluate(() => localStorage.setItem('theme', 'light')); - await page.goto('/'); - - const isLightMode = await page.evaluate(() => document.body.classList.contains('light-mode')); - expect(isLightMode).toBe(true); + test("should navigate to Settings", async ({ page }) => { + await page.goto("/settings"); + expect(await page.textContent("h1")).toBe("Settings"); }); - test('should respect system theme preference if no stored theme', async ({ page }) => { - await page.goto('/'); // Clear any localStorage state if needed - await page.evaluate(() => localStorage.removeItem('theme')); - - const systemPrefersDark = await page.evaluate(() => window.matchMedia('(prefers-color-scheme: dark)').matches); - const isDarkMode = await page.evaluate(() => document.body.classList.contains('dark-mode')); - expect(isDarkMode).toBe(systemPrefersDark); + test("should show Not Found for undefined routes", async ({ page }) => { + await page.goto("/undefined-route"); + expect(await page.textContent("h1")).toBe("404 - Page Not Found"); }); }); diff --git a/javascript/dwa-starter-vanillajs-vite/tests/offline.spec.js b/javascript/dwa-starter-vanillajs-vite/tests/offline.spec.js new file mode 100644 index 00000000..63c9bdab --- /dev/null +++ b/javascript/dwa-starter-vanillajs-vite/tests/offline.spec.js @@ -0,0 +1,18 @@ +// // tests/offline.spec.js +import { test, expect } from "@playwright/test"; + +test("App should load and function offline", async ({ page, context }) => { + // Load the app online to cache assets + await page.goto("http://localhost:5173"); + await expect(page.locator("body")).toBeVisible(); + + // Enable offline mode + await context.setOffline(true); + + // Confirm that offline content is accessible + const pageContent = await page.evaluate(() => document.body.innerText); + expect(pageContent).toContain("Home"); // Check content that’s expected offline + + // Disable offline mode to reset conditions + await context.setOffline(false); +}); diff --git a/javascript/dwa-starter-vanillajs-vite/tests/sw.spec.js b/javascript/dwa-starter-vanillajs-vite/tests/sw.spec.js new file mode 100644 index 00000000..cc4910fe --- /dev/null +++ b/javascript/dwa-starter-vanillajs-vite/tests/sw.spec.js @@ -0,0 +1,22 @@ +// tests/sw.spec.js + +import { test, expect } from "@playwright/test"; + +test("Service worker should be registered and ready on page load", async ({ + page, +}) => { + // Go to the app's base URL + await page.goto("http://localhost:5173"); + + // Wait for the service worker to register and become active + const swRegisteredAndActive = await page.evaluate(() => { + return new Promise((resolve) => { + navigator.serviceWorker.ready.then((registration) => { + resolve(!!registration.active); + }); + }); + }); + + // Assert that the service worker is both registered and active + expect(swRegisteredAndActive).toBeTruthy(); +});