diff --git a/tinytorch/site/extra/community/.gitignore b/tinytorch/site/extra/community/.gitignore new file mode 100644 index 000000000..b5b06f31b --- /dev/null +++ b/tinytorch/site/extra/community/.gitignore @@ -0,0 +1,8 @@ +# Secrets +tests/e2e/credentials.json + +# Playwright +test-results/ +playwright-report/ +blob-report/ +playwright/.cache/ diff --git a/tinytorch/site/extra/community/app.js b/tinytorch/site/extra/community/app.js index 9b55e7e61..c2144e8dc 100644 --- a/tinytorch/site/extra/community/app.js +++ b/tinytorch/site/extra/community/app.js @@ -1,7 +1,7 @@ import { injectStyles } from './modules/styles.js'; -import { renderLayout, updateNavState } from './modules/ui.js?v=3'; -import { getSession } from './modules/state.js?v=2'; -import { openModal, closeModal, handleToggle, handleAuth, handleLogout, setMode, verifySession, signInWithSocial, supabase } from './modules/auth.js?v=3'; +import { renderLayout, updateNavState } from './modules/ui.js'; +import { getSession } from './modules/state.js'; +import { openModal, closeModal, handleToggle, handleAuth, handleLogout, setMode, verifySession, signInWithSocial, supabase } from './modules/auth.js'; import { openProfileModal, closeProfileModal, handleProfileUpdate, geocodeAndSetCoordinates, checkAndAutoUpdateLocation, setupProfileDeleteEvents } from './modules/profile.js'; import { setupCameraEvents } from './modules/camera.js'; import { getBasePath } from './modules/config.js'; @@ -35,38 +35,24 @@ import { getBasePath } from './modules/config.js'; // Initialize profile events setupProfileDeleteEvents(); - // 2.6 Check for Supabase Session & Verify - const checkProfile = async (session) => { - if (!session || window.location.pathname.includes('profile_setup')) return; - - const { data: profile } = await supabase - .from('profiles') - .select('display_name, institution, location') - .eq('id', session.user.id) - .single(); - - const hasName = profile && profile.display_name; - const hasInst = profile && profile.institution && (Array.isArray(profile.institution) ? profile.institution.length > 0 : !!profile.institution); - const hasLoc = profile && profile.location; - - if (!hasName || !hasInst || !hasLoc) { - window.location.href = getBasePath() + '/profile_setup.html'; - } - }; - supabase.auth.getSession().then(({ data: { session } }) => { if (session) { localStorage.setItem("tinytorch_token", session.access_token); if (session.refresh_token) localStorage.setItem("tinytorch_refresh_token", session.refresh_token); if (session.user) localStorage.setItem("tinytorch_user", JSON.stringify(session.user)); - // Clean URL hash if present (Supabase puts tokens there) - if (window.location.hash && window.location.hash.includes('access_token')) { - window.history.replaceState({}, document.title, window.location.pathname + window.location.search); + // Clean URL of tokens/code (Supabase puts tokens in hash or code in query) + if ((window.location.hash && window.location.hash.includes('access_token')) || + (window.location.search && window.location.search.includes('code='))) { + // Remove sensitive parameters while preserving non-sensitive ones + const url = new URL(window.location); + url.hash = ''; + url.searchParams.delete('code'); + url.searchParams.delete('type'); + window.history.replaceState({}, document.title, url.toString()); } updateNavState(); - checkProfile(session); } // 3. Verify Session (Async) verifySession(); @@ -82,7 +68,6 @@ import { getBasePath } from './modules/config.js'; if (session.refresh_token) localStorage.setItem("tinytorch_refresh_token", session.refresh_token); if (session.user) localStorage.setItem("tinytorch_user", JSON.stringify(session.user)); updateNavState(); - checkProfile(session); } else if (event === 'SIGNED_OUT') { localStorage.removeItem("tinytorch_token"); updateNavState(); @@ -171,10 +156,13 @@ import { getBasePath } from './modules/config.js'; const action = params.get('action'); if (action === 'login') { - localStorage.removeItem("tinytorch_token"); - localStorage.removeItem("tinytorch_refresh_token"); - localStorage.removeItem("tinytorch_user"); - updateNavState(); + const { isLoggedIn } = getSession(); + if (!isLoggedIn) { + localStorage.removeItem("tinytorch_token"); + localStorage.removeItem("tinytorch_refresh_token"); + localStorage.removeItem("tinytorch_user"); + updateNavState(); + } openModal('login'); } else if (action === 'profile') { const { isLoggedIn } = getSession(); diff --git a/tinytorch/site/extra/community/community.html b/tinytorch/site/extra/community/community.html index d01d87e7b..5ecd8dc53 100644 --- a/tinytorch/site/extra/community/community.html +++ b/tinytorch/site/extra/community/community.html @@ -289,15 +289,18 @@
- 0 Members + 0 Members on Map +
+
+ + 0 without Location (Lost at Sea)
0 Institutions
-
- - Members with Unknown Location +
+ Note: Members without a location set in their profile are placed at the Sea Station.
@@ -307,7 +310,6 @@ - - // Handle Window Resize - window.addEventListener('resize', () => { - const w = window.innerWidth; - const h = window.innerHeight; - svg.attr('width', w).attr('height', h); - projection.translate([w/2, h/2]).scale(Math.min(w, h) / 2.5); - center[0] = w/2; - center[1] = h/2; + + // Update static globe background + globeGroup.select("path").attr("d", path); + redraw(); }); diff --git a/tinytorch/site/extra/community/modules/auth.js b/tinytorch/site/extra/community/modules/auth.js index c72d0cc75..0e3ca7862 100644 --- a/tinytorch/site/extra/community/modules/auth.js +++ b/tinytorch/site/extra/community/modules/auth.js @@ -1,11 +1,8 @@ import { NETLIFY_URL, SUPABASE_URL, SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY, getBasePath } from './config.js'; -import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm'; -import { updateNavState } from './ui.js?v=2'; +import { updateNavState } from './ui.js'; import { closeProfileModal, openProfileModal } from './profile.js'; -import { getSession, forceLogin, clearSession } from './state.js?v=2'; +import { getSession, forceLogin, clearSession, supabase } from './state.js'; -// Initialize Supabase Client -const supabase = createClient(SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY); export { supabase }; export async function signInWithSocial(provider) { @@ -267,20 +264,51 @@ export async function handleAuth(e) { authSubmit.innerHTML = '
'; try { - let endpoint, body; - - if (currentMode === 'forgot') { - endpoint = '/api/auth/reset-password'; - body = { email }; - } else { - endpoint = currentMode === 'login' ? '/api/auth/login' : '/api/auth/signup'; - body = { + // Direct Supabase flow for Signup + if (currentMode === 'signup') { + const redirectUrl = window.location.origin + basePath + '/index.html?action=login&confirmed_email=true'; + console.log("Requesting signup with redirect to:", redirectUrl); + + const { error } = await supabase.auth.signUp({ email, password, - redirect_to: window.location.origin + basePath + '/index.html?action=login&confirmed_email=true' - }; + options: { + emailRedirectTo: redirectUrl + } + }); + if (error) throw error; + + closeModal(); + showMessageModal( + 'Check your Email', + 'If you don\'t already have an account, we have sent you an email. Please check your inbox to confirm your signup.' + ); + return; } + // Direct Supabase flow for Password Reset + if (currentMode === 'forgot') { + const redirectUrl = window.location.origin + basePath + '/index.html?action=reset-password'; + console.log("Requesting password reset with redirect to:", redirectUrl); + + const { error } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: redirectUrl + }); + if (error) throw error; + + showMessageModal('Reset Link Sent', 'If an account exists, a reset link has been sent.'); + setMode('login'); + return; + } + + // Keep existing API flow for Login + const endpoint = '/api/auth/login'; + const body = { + email, + password, + redirect_to: window.location.origin + basePath + '/index.html?action=login&confirmed_email=true' + }; + const url = `${NETLIFY_URL}${endpoint}`; const response = await fetch(url, { @@ -294,72 +322,60 @@ export async function handleAuth(e) { const data = await response.json(); - if (currentMode === 'forgot') { - if (response.ok) { - showMessageModal('Reset Link Sent', data.message || 'If an account exists, a reset link has been sent.'); - setMode('login'); - } else { - throw new Error(data.error || 'Failed to send reset link'); + if (!response.ok) { + throw new Error(data.error || 'Login failed'); + } + + if (data.access_token) { + localStorage.setItem("tinytorch_token", data.access_token); + if (data.refresh_token) localStorage.setItem("tinytorch_refresh_token", data.refresh_token); + localStorage.setItem("tinytorch_user", JSON.stringify(data.user)); + + // Sync Supabase Client so it doesn't trigger SIGNED_OUT + if (data.refresh_token) { + const { error: sessionError } = await supabase.auth.setSession({ + access_token: data.access_token, + refresh_token: data.refresh_token + }); + if (sessionError) { + console.error("Supabase setSession error during login:", sessionError); } - } else { - if (!response.ok) { - throw new Error(data.error || (currentMode === 'login' ? 'Login failed' : 'Signup failed')); } - if (currentMode === 'login') { - if (data.access_token) { - localStorage.setItem("tinytorch_token", data.access_token); - if (data.refresh_token) localStorage.setItem("tinytorch_refresh_token", data.refresh_token); - localStorage.setItem("tinytorch_user", JSON.stringify(data.user)); + updateNavState(); - // Sync Supabase Client so it doesn't trigger SIGNED_OUT - if (data.refresh_token) { - const { error: sessionError } = await supabase.auth.setSession({ - access_token: data.access_token, - refresh_token: data.refresh_token - }); - if (sessionError) { - console.error("Supabase setSession error during login:", sessionError); - } - } + // Check Profile Completeness immediately + const { data: profile } = await supabase + .from('profiles') + .select('display_name, institution, location') + .eq('id', data.user.id) + .single(); - updateNavState(); + const hasName = profile && profile.display_name; + const hasInst = profile && profile.institution && (Array.isArray(profile.institution) ? profile.institution.length > 0 : !!profile.institution); + const hasLoc = profile && profile.location; - // Check Profile Completeness immediately - const { data: profile } = await supabase - .from('profiles') - .select('display_name, institution, location') - .eq('id', data.user.id) - .single(); + const params = new URLSearchParams(window.location.search); + const nextParam = params.get('next'); + + closeModal(); // Always close modal on success - const hasName = profile && profile.display_name; - const hasInst = profile && profile.institution && (Array.isArray(profile.institution) ? profile.institution.length > 0 : !!profile.institution); - const hasLoc = profile && profile.location; + if (nextParam) { + // Ensure nextParam is a clean path if it was encoded + const cleanNext = decodeURIComponent(nextParam).split('?')[0]; + window.location.href = cleanNext; + return; + } - if (!hasName || !hasInst || !hasLoc) { - window.location.href = basePath + '/profile_setup.html'; - return; - } + if (!hasName || !hasInst || !hasLoc) { + window.location.href = basePath + '/profile_setup.html'; + return; + } - const params = new URLSearchParams(window.location.search); - if (params.get('action') === 'profile') { - closeModal(); - openProfileModal(); - } else { - window.location.href = basePath + '/dashboard.html'; - } - } + if (params.get('action') === 'profile') { + openProfileModal(); } else { - // Signup Success - Show Message Modal - // We close the auth modal first so it doesn't overlap - closeModal(); - showMessageModal( - 'Check your Email', - 'If you don\'t already have an account, we have sent you an email. Please check your inbox to confirm your signup.', - () => { - window.location.href = basePath + '/dashboard.html'; - } - ); + window.location.href = basePath + '/dashboard.html'; } } diff --git a/tinytorch/site/extra/community/modules/candle.js b/tinytorch/site/extra/community/modules/candle.js index 1a279aa79..2e787f11f 100644 --- a/tinytorch/site/extra/community/modules/candle.js +++ b/tinytorch/site/extra/community/modules/candle.js @@ -1,8 +1,17 @@ // --- CANDLE ANIMATION MODULE --- +const activeLoops = new Set(); + export function initCandle(canvasId) { const canvas = document.getElementById(canvasId); if (!canvas) return; + // Prevent multiple loops on the same canvas + if (activeLoops.has(canvasId)) { + console.log(`Candle loop already running for ${canvasId}, skipping init.`); + return; + } + activeLoops.add(canvasId); + const ctx = canvas.getContext("2d"); // Coordinate system setup: diff --git a/tinytorch/site/extra/community/modules/guard.js b/tinytorch/site/extra/community/modules/guard.js index 9f025f6fb..34091925f 100644 --- a/tinytorch/site/extra/community/modules/guard.js +++ b/tinytorch/site/extra/community/modules/guard.js @@ -1,81 +1,92 @@ -import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm'; import { SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY, SUPABASE_URL, getBasePath } from './config.js'; +import { clearSession, supabase } from './state.js'; -const supabase = createClient(SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY); const LOGIN_PAGE = getBasePath() + '/index.html'; const SETUP_PAGE = getBasePath() + '/profile_setup.html'; +const DASHBOARD_PAGE = getBasePath() + '/dashboard.html'; (async function guard() { - // 0. HYDRATE SESSION (Fix for Direct Email Login) + const path = window.location.pathname; + // Enhanced path checking for landing page + const isOnIndex = path.endsWith('/') || path.endsWith('index.html') || path === getBasePath() || path === getBasePath() + '/'; + const isOnSetupPage = path.includes('profile_setup.html'); + const isPublicPage = isOnIndex || path.includes('login') || path.includes('about') || path.includes('contact'); + const isProtected = !isPublicPage; + + // 0. Get Session const storedToken = localStorage.getItem("tinytorch_token"); const storedRefresh = localStorage.getItem("tinytorch_refresh_token"); + if (storedToken && storedRefresh) { - await supabase.auth.setSession({ - access_token: storedToken, - refresh_token: storedRefresh - }); + try { + await supabase.auth.setSession({ + access_token: storedToken, + refresh_token: storedRefresh + }); + } catch (e) { + console.warn("Guard: setSession failed", e); + } } - // 1. Check Session (Supabase Client) const { data: { session } } = await supabase.auth.getSession(); - let profile = null; - - if (session) { - // 2a. Fetch Profile via Client - const { data } = await supabase - .from('profiles') - .select('display_name, institution, location') - .eq('id', session.user.id) - .single(); - profile = data; - } else { - // 1b. Fallback: Check Token Manually - if (!storedToken) { - // No session, no token -> Redirect - if (!window.location.pathname.includes('index') && !window.location.pathname.includes('login') && !window.location.pathname.includes('about')) { - window.location.href = LOGIN_PAGE + '?action=login&next=' + encodeURIComponent(window.location.pathname); - } - return; - } - - // Have token, verify via API - try { - const res = await fetch(`${SUPABASE_URL}/get-profile-details`, { - headers: { 'Authorization': `Bearer ${storedToken}` } - }); - - if (!res.ok) { - throw new Error("Token invalid"); - } - const data = await res.json(); - profile = data.profile; // API returns { profile: {...}, completed_modules: [...] } - - } catch (e) { - console.warn("Guard: Token validation failed", e); - if (!window.location.pathname.includes('index') && !window.location.pathname.includes('login')) { - window.location.href = LOGIN_PAGE + '?action=login&next=' + encodeURIComponent(window.location.pathname); - } - return; - } + if (!session && isProtected) { + console.log("๐Ÿšง No session on protected page. Redirecting to login..."); + window.location.href = LOGIN_PAGE + '?action=login&next=' + encodeURIComponent(path); + return; } - // 3. The Rules - // Must have ALL three: Name, Institution, Location + if (!session) return; // Public page, no session, we are fine. + + // 1. Fetch Profile with Timeout + let profile = null; + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + try { + // Use the API for profile details + const res = await fetch(`${SUPABASE_URL}/get-profile-details`, { + headers: { 'Authorization': `Bearer ${session.access_token}` }, + signal: controller.signal + }); + clearTimeout(timeoutId); + + if (res.ok) { + const data = await res.json(); + profile = data.profile; + } else if (res.status === 401 || res.status === 404) { + // 401: Token expired, 404: User deleted from DB + console.warn(`Guard: Session invalid or account deleted (${res.status}). Purging...`); + await clearSession(); + + if (isProtected) { + window.location.href = LOGIN_PAGE + '?action=login'; + } else { + // If on a public page, just reload to clear UI state + window.location.reload(); + } + return; + } + } catch (e) { + console.warn("Guard: Profile fetch failed or timed out", e); + } + + // 2. The Rules const hasName = profile && profile.display_name; const hasInst = profile && profile.institution && (Array.isArray(profile.institution) ? profile.institution.length > 0 : !!profile.institution); const hasLoc = profile && profile.location; const isComplete = hasName && hasInst && hasLoc; - const isOnSetupPage = window.location.pathname.includes('profile_setup'); - if (!isComplete && !isOnSetupPage) { - console.log("๐Ÿšง Profile incomplete. Redirecting to setup..."); - window.location.href = SETUP_PAGE; - } - else if (isComplete && isOnSetupPage) { - // If they are done but try to visit setup, send them to dashboard - window.location.href = getBasePath() + '/dashboard.html'; + if (isComplete) { + if (isOnSetupPage) { + console.log("โœ… Profile complete. Moving to dashboard..."); + window.location.href = DASHBOARD_PAGE; + } + } else { + if (!isOnSetupPage && isProtected) { + console.log("๐Ÿšง Profile incomplete. Redirecting to setup..."); + window.location.href = SETUP_PAGE; + } } - })(); diff --git a/tinytorch/site/extra/community/modules/profile.js b/tinytorch/site/extra/community/modules/profile.js index ae7e0c611..f0ae9c1cc 100644 --- a/tinytorch/site/extra/community/modules/profile.js +++ b/tinytorch/site/extra/community/modules/profile.js @@ -1,5 +1,5 @@ import { SUPABASE_URL, NETLIFY_URL, getBasePath } from './config.js'; -import { forceLogin, getSession } from './state.js?v=2'; +import { forceLogin, getSession, clearSession } from './state.js'; import { initCandle } from './candle.js'; export async function geocodeAndSetCoordinates(location) { @@ -124,9 +124,7 @@ export function setupProfileDeleteEvents() { } alert("Your account has been deleted."); - localStorage.removeItem("tinytorch_token"); - localStorage.removeItem("tinytorch_refresh_token"); - localStorage.removeItem("tinytorch_user"); + await clearSession(); window.location.href = getBasePath() + '/index.html'; } catch (error) { console.error("Delete account error:", error); diff --git a/tinytorch/site/extra/community/modules/state.js b/tinytorch/site/extra/community/modules/state.js index 662b663ac..6ea585a30 100644 --- a/tinytorch/site/extra/community/modules/state.js +++ b/tinytorch/site/extra/community/modules/state.js @@ -1,4 +1,16 @@ import { getBasePath } from './config.js'; +import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm'; +import { SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY } from './config.js'; + +const supabase = createClient(SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY, { + auth: { + flowType: 'pkce', // Prefer PKCE for security, or keep 'implicit' if standard for this app + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: true + } +}); +export { supabase }; // State Management export function getSession() { @@ -12,10 +24,46 @@ export function getSession() { return { token, email, isLoggedIn: !!token }; } -export function clearSession() { +export async function clearSession() { + console.log("๐Ÿงน Aggressively clearing session and cookies..."); + try { + await supabase.auth.signOut(); + } catch (e) { + console.warn("Supabase signOut error during clearSession:", e); + } + + // 1. Explicitly remove our own keys localStorage.removeItem("tinytorch_token"); localStorage.removeItem("tinytorch_refresh_token"); localStorage.removeItem("tinytorch_user"); + sessionStorage.removeItem("tinytorch_location_checked"); + + // 2. Clear all Supabase and auth-related keys from localStorage + const keysToRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && ( + key.includes("supabase") || + key.includes("auth-token") || + key.startsWith("sb-") + )) { + keysToRemove.push(key); + } + } + keysToRemove.forEach(k => localStorage.removeItem(k)); + + // 3. Clear all auth-related cookies + const cookies = document.cookie.split(";"); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i]; + const eqPos = cookie.indexOf("="); + const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); + // Clear for all common paths and subdomains + document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=" + window.location.hostname; + document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/"; + } + + console.log("โœจ Session cleared."); } export function forceLogin() { diff --git a/tinytorch/site/extra/community/modules/ui.js b/tinytorch/site/extra/community/modules/ui.js index 7d5e6fe1f..7f2c4fd2e 100644 --- a/tinytorch/site/extra/community/modules/ui.js +++ b/tinytorch/site/extra/community/modules/ui.js @@ -1,5 +1,5 @@ import { getBasePath, NETLIFY_URL } from './config.js'; -import { getSession } from './state.js?v=2'; +import { getSession } from './state.js'; export function updateNavState() { const { isLoggedIn, email: userEmail } = getSession(); diff --git a/tinytorch/site/extra/community/package-lock.json b/tinytorch/site/extra/community/package-lock.json new file mode 100644 index 000000000..aadc8413b --- /dev/null +++ b/tinytorch/site/extra/community/package-lock.json @@ -0,0 +1,78 @@ +{ + "name": "tinytorch-community-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tinytorch-community-tests", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "^1.42.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/tinytorch/site/extra/community/package.json b/tinytorch/site/extra/community/package.json new file mode 100644 index 000000000..07c8343c1 --- /dev/null +++ b/tinytorch/site/extra/community/package.json @@ -0,0 +1,12 @@ +{ + "name": "tinytorch-community-tests", + "version": "1.0.0", + "description": "E2E tests for Tiny Torch Community", + "scripts": { + "test": "npx playwright test", + "test:ui": "npx playwright test --ui" + }, + "devDependencies": { + "@playwright/test": "^1.42.0" + } +} diff --git a/tinytorch/site/extra/community/playwright.config.js b/tinytorch/site/extra/community/playwright.config.js new file mode 100644 index 000000000..665e043c8 --- /dev/null +++ b/tinytorch/site/extra/community/playwright.config.js @@ -0,0 +1,21 @@ +const { defineConfig, devices } = require('@playwright/test'); + +module.exports = defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:8000/community/', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/tinytorch/site/extra/community/profile_setup.html b/tinytorch/site/extra/community/profile_setup.html index 1bc3bfadd..c3637b885 100644 --- a/tinytorch/site/extra/community/profile_setup.html +++ b/tinytorch/site/extra/community/profile_setup.html @@ -307,9 +307,10 @@