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 @@
+
+
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();
+});