diff --git a/playwright.config.ts b/playwright.config.ts index 046d650..5f6ed52 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -24,21 +24,25 @@ export default defineConfig({ /* Configure projects for major browsers */ projects: [ // Setup project - { name: 'setup', testMatch: /.*\.setup\.ts/ }, + { name: 'setup', testMatch: /setup-admin\.setup\.ts/ }, + // logout project + { + name: 'logout', + testMatch: /logout\.spec\.ts/, + }, { name: 'chromium', use: { ...devices['Desktop Chrome'], - storageState: 'tests/.auth/user.json', + storageState: 'tests/.auth/admin.json', }, dependencies: ['setup'], }, - { name: 'firefox', use: { ...devices['Desktop Firefox'], - storageState: 'tests/.auth/user.json', + storageState: 'tests/.auth/admin.json', }, dependencies: ['setup'], }, @@ -47,7 +51,7 @@ export default defineConfig({ name: 'webkit', use: { ...devices['Desktop Safari'], - storageState: 'tests/.auth/user.json', + storageState: 'tests/.auth/admin.json', }, dependencies: ['setup'], }, @@ -57,6 +61,6 @@ export default defineConfig({ command: 'npm run build && npm run start', port: 3000, reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, + timeout: 200 * 1000, }, }) diff --git a/src/actions/orders.ts b/src/actions/orders.ts index 3be2582..6dfcba1 100644 --- a/src/actions/orders.ts +++ b/src/actions/orders.ts @@ -28,10 +28,10 @@ export const updateOrderStatus = async (orderId: number, status: string) => { // 查询用户信息:userId const { - data: { session }, - } = await supabase.auth.getSession() + data: { user }, + } = await supabase.auth.getUser() - const userId = session?.user.id + const userId = user?.id if (!userId) throw new Error('User not found') diff --git a/src/app/admin/categories/category-form.tsx b/src/app/admin/categories/category-form.tsx index 68e211d..ccd98ad 100644 --- a/src/app/admin/categories/category-form.tsx +++ b/src/app/admin/categories/category-form.tsx @@ -103,7 +103,9 @@ export const CategoryForm = ({ try { return await imageUploadHandler(formData) } catch { - toast.error('Image upload failed!') + toast.error('Image upload failed!', { + position: 'top-right', + }) return null } } @@ -122,13 +124,17 @@ export const CategoryForm = ({ imageUrl, slug: defaultValues?.slug || slug, // 如果已有 slug,则沿用;否则根据名称生成 }) - toast.success('Category updated successfully') + toast.success('Category updated successfully', { + position: 'top-right', + }) } else { // 创建新分类 const imageUrl = await handleImageUpload() if (imageUrl) { await createCategory({ imageUrl, name }) - toast.success('Category created successfully') + toast.success('Category created successfully', { + position: 'top-right', + }) } } form.reset() diff --git a/src/app/admin/categories/category-table-row.tsx b/src/app/admin/categories/category-table-row.tsx index f9e4a0c..b68549f 100644 --- a/src/app/admin/categories/category-table-row.tsx +++ b/src/app/admin/categories/category-table-row.tsx @@ -60,9 +60,13 @@ export const CategoryTableRow = ({ // 删除分类 try { await deleteCategory(id) - toast.success('Category deleted successfully') + toast.success('Category deleted successfully', { + position: 'top-right', + }) } catch { - toast.error('Failed to delete category') + toast.error('Failed to delete category', { + position: 'top-right', + }) } // 刷新页面 diff --git a/src/app/admin/products/page-component.tsx b/src/app/admin/products/page-component.tsx index a4d3e4e..e807bc7 100644 --- a/src/app/admin/products/page-component.tsx +++ b/src/app/admin/products/page-component.tsx @@ -53,7 +53,9 @@ export const ProductPageComponent: FC = ({ if (currentProduct?.slug) { await deleteProduct(currentProduct.slug) router.refresh() - toast.success('Product deleted successfully') + toast.success('Product deleted successfully', { + position: 'top-right', + }) setIsDeleteModalOpen(false) setCurrentProduct(null) } diff --git a/src/app/admin/products/product-dialog-form.tsx b/src/app/admin/products/product-dialog-form.tsx index 56f20fe..4a41cd0 100644 --- a/src/app/admin/products/product-dialog-form.tsx +++ b/src/app/admin/products/product-dialog-form.tsx @@ -167,13 +167,17 @@ export const ProductForm = ({ // 检查主图是否成功上传 if (!uploadedHeroImage) { - toast.error('Failed to upload the hero image. Please try again.') + toast.error('Failed to upload the hero image. Please try again.', { + position: 'top-right', + }) return } // 检查是否所有产品图片都成功上传 if (uploadedImages.includes(null)) { - toast.error('Failed to upload some product images. Please try again.') + toast.error('Failed to upload some product images. Please try again.', { + position: 'top-right', + }) return } @@ -203,14 +207,19 @@ export const ProductForm = ({ toast.success( isEditMode ? 'Product updated successfully!' - : 'Product created successfully!' + : 'Product created successfully!', + { + position: 'top-right', + } ) form.reset() // 重置表单 router.refresh() // 刷新页面 setIsProductModalOpen(false) // 关闭模态框 } catch (error) { console.error(error) - toast.error('Something went wrong. Please try again.') + toast.error('Something went wrong. Please try again.', { + position: 'top-right', + }) } } diff --git a/src/app/admin/products/schema.ts b/src/app/admin/products/schema.ts index 720bc66..901235c 100644 --- a/src/app/admin/products/schema.ts +++ b/src/app/admin/products/schema.ts @@ -5,11 +5,11 @@ export const createOrUpdateProductSchema = z.object({ price: z .number() .int({ message: 'Price must be an integer' }) - .min(0, { message: 'Price must be a positive integer' }), + .positive({ message: 'Price is required' }), maxQuantity: z .number() .int({ message: 'Max Quantity must be an integer' }) - .min(0, { message: 'Max Quantity must be a positive integer' }), + .positive({ message: 'Max Quantity is required' }), category: z.string().min(1, { message: 'Category is required' }), heroImage: z .union([ diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx index 6d36da5..934ab87 100644 --- a/src/app/auth/page.tsx +++ b/src/app/auth/page.tsx @@ -58,9 +58,13 @@ export default function Auth() { } catch (error) { // 捕获到异常,提示异常中的错误信息 if (error instanceof Error) { - toast.error(error.message) + toast.error(error.message, { + position: 'top-right', + }) } else { - toast.error('An error occurred while authenticating') + toast.error('An error occurred while authenticating', { + position: 'top-right', + }) } } finally { setIsAuthenticating(false) diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 9b18eb6..4d225a6 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -1 +1,2 @@ export const ADMIN = 'admin' +export const USER = 'user' diff --git a/tests/.auth/admin.json b/tests/.auth/admin.json new file mode 100644 index 0000000..1945f51 --- /dev/null +++ b/tests/.auth/admin.json @@ -0,0 +1,15 @@ +{ + "cookies": [ + { + "name": "sb-binfgbgsoetsbeapeoks-auth-token", + "value": "base64-eyJhY2Nlc3NfdG9rZW4iOiJleUpoYkdjaU9pSklVekkxTmlJc0ltdHBaQ0k2SWpsa1RXazVhSFJXWjFsNFRXMXpkSEVpTENKMGVYQWlPaUpLVjFRaWZRLmV5SnBjM01pT2lKb2RIUndjem92TDJKcGJtWm5ZbWR6YjJWMGMySmxZWEJsYjJ0ekxuTjFjR0ZpWVhObExtTnZMMkYxZEdndmRqRWlMQ0p6ZFdJaU9pSmhNREF6WXpBeE55MHlNV1F6TFRReFpqWXRPRE0yTkMwMVpUbGxaV0psWWprek5XUWlMQ0poZFdRaU9pSmhkWFJvWlc1MGFXTmhkR1ZrSWl3aVpYaHdJam94TnpNek5Ea3dOREV6TENKcFlYUWlPakUzTXpNME9EWTRNVE1zSW1WdFlXbHNJam9pZEdWemRFQmhaRzFwYmk1amIyMGlMQ0p3YUc5dVpTSTZJaUlzSW1Gd2NGOXRaWFJoWkdGMFlTSTZleUp3Y205MmFXUmxjaUk2SW1WdFlXbHNJaXdpY0hKdmRtbGtaWEp6SWpwYkltVnRZV2xzSWwxOUxDSjFjMlZ5WDIxbGRHRmtZWFJoSWpwN0ltVnRZV2xzSWpvaWRHVnpkRUJoWkcxcGJpNWpiMjBpTENKbGJXRnBiRjkyWlhKcFptbGxaQ0k2Wm1Gc2MyVXNJbkJvYjI1bFgzWmxjbWxtYVdWa0lqcG1ZV3h6WlN3aWMzVmlJam9pWVRBd00yTXdNVGN0TWpGa015MDBNV1kyTFRnek5qUXROV1U1WldWaVpXSTVNelZrSW4wc0luSnZiR1VpT2lKaGRYUm9aVzUwYVdOaGRHVmtJaXdpWVdGc0lqb2lZV0ZzTVNJc0ltRnRjaUk2VzNzaWJXVjBhRzlrSWpvaWNHRnpjM2R2Y21RaUxDSjBhVzFsYzNSaGJYQWlPakUzTXpNME9EWTRNVE45WFN3aWMyVnpjMmx2Ymw5cFpDSTZJbVZrWW1WaU1XSmxMVGMxTVRBdE5EUmtaUzA0T0RZMUxXSXdOMkV4T0RRMk5ESTVNeUlzSW1selgyRnViMjU1Ylc5MWN5STZabUZzYzJWOS5yNU9hZW8wckRvUk1CeFlZSWZZMFRIaUd0LVVDcFZaTjhmNmpiOW9SOG5nIiwidG9rZW5fdHlwZSI6ImJlYXJlciIsImV4cGlyZXNfaW4iOjM2MDAsImV4cGlyZXNfYXQiOjE3MzM0OTA0MTMsInJlZnJlc2hfdG9rZW4iOiJPb0NPT3lwdGVRdFNWZjFXMHlJZXd3IiwidXNlciI6eyJpZCI6ImEwMDNjMDE3LTIxZDMtNDFmNi04MzY0LTVlOWVlYmViOTM1ZCIsImF1ZCI6ImF1dGhlbnRpY2F0ZWQiLCJyb2xlIjoiYXV0aGVudGljYXRlZCIsImVtYWlsIjoidGVzdEBhZG1pbi5jb20iLCJlbWFpbF9jb25maXJtZWRfYXQiOiIyMDI0LTEyLTA2VDA4OjUwOjM0LjUzMDU2NVoiLCJwaG9uZSI6IiIsImNvbmZpcm1lZF9hdCI6IjIwMjQtMTItMDZUMDg6NTA6MzQuNTMwNTY1WiIsImxhc3Rfc2lnbl9pbl9hdCI6IjIwMjQtMTItMDZUMTI6MDY6NTMuMzQ0MTc2NzA0WiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7ImVtYWlsIjoidGVzdEBhZG1pbi5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInBob25lX3ZlcmlmaWVkIjpmYWxzZSwic3ViIjoiYTAwM2MwMTctMjFkMy00MWY2LTgzNjQtNWU5ZWViZWI5MzVkIn0sImlkZW50aXRpZXMiOlt7ImlkZW50aXR5X2lkIjoiMGUyZGIzZTYtMWIzMi00MWI4LTlmNWMtMGZhMjdlOTExMWE2IiwiaWQiOiJhMDAzYzAxNy0yMWQzLTQxZjYtODM2NC01ZTllZWJlYjkzNWQiLCJ1c2VyX2lkIjoiYTAwM2MwMTctMjFkMy00MWY2LTgzNjQtNWU5ZWViZWI5MzVkIiwiaWRlbnRpdHlfZGF0YSI6eyJlbWFpbCI6InRlc3RAYWRtaW4uY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwaG9uZV92ZXJpZmllZCI6ZmFsc2UsInN1YiI6ImEwMDNjMDE3LTIxZDMtNDFmNi04MzY0LTVlOWVlYmViOTM1ZCJ9LCJwcm92aWRlciI6ImVtYWlsIiwibGFzdF9zaWduX2luX2F0IjoiMjAyNC0xMi0wNlQwODo1MDozNC41MjQ2NVoiLCJjcmVhdGVkX2F0IjoiMjAyNC0xMi0wNlQwODo1MDozNC41MjQ3MDhaIiwidXBkYXRlZF9hdCI6IjIwMjQtMTItMDZUMDg6NTA6MzQuNTI0NzA4WiIsImVtYWlsIjoidGVzdEBhZG1pbi5jb20ifV0sImNyZWF0ZWRfYXQiOiIyMDI0LTEyLTA2VDA4OjUwOjM0LjQ4OTA2OFoiLCJ1cGRhdGVkX2F0IjoiMjAyNC0xMi0wNlQxMjowNjo1My4zNDcxMDJaIiwiaXNfYW5vbnltb3VzIjpmYWxzZX19", + "domain": "localhost", + "path": "/", + "expires": 1768046813.496824, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + } + ], + "origins": [] +} \ No newline at end of file diff --git a/tests/.auth/logout.json b/tests/.auth/logout.json new file mode 100644 index 0000000..f4ec355 --- /dev/null +++ b/tests/.auth/logout.json @@ -0,0 +1,4 @@ +{ + "cookies": [], + "origins": [] +} \ No newline at end of file diff --git a/tests/.auth/user.json b/tests/.auth/user.json deleted file mode 100644 index 72cc5f4..0000000 --- a/tests/.auth/user.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "cookies": [ - { - "name": "sb-binfgbgsoetsbeapeoks-auth-token", - "value": "base64-eyJhY2Nlc3NfdG9rZW4iOiJleUpoYkdjaU9pSklVekkxTmlJc0ltdHBaQ0k2SWpsa1RXazVhSFJXWjFsNFRXMXpkSEVpTENKMGVYQWlPaUpLVjFRaWZRLmV5SnBjM01pT2lKb2RIUndjem92TDJKcGJtWm5ZbWR6YjJWMGMySmxZWEJsYjJ0ekxuTjFjR0ZpWVhObExtTnZMMkYxZEdndmRqRWlMQ0p6ZFdJaU9pSmpNVGsyT1dOa1ppMWtNVFE0TFRReE1XUXRPRGMxTXkxaVpXSXdNR1ZoWkRRellUY2lMQ0poZFdRaU9pSmhkWFJvWlc1MGFXTmhkR1ZrSWl3aVpYaHdJam94TnpNek16a3lNVFl3TENKcFlYUWlPakUzTXpNek9EZzFOakFzSW1WdFlXbHNJam9pZEdWemRFQjBaWE4wTG1OdmJTSXNJbkJvYjI1bElqb2lJaXdpWVhCd1gyMWxkR0ZrWVhSaElqcDdJbkJ5YjNacFpHVnlJam9pWlcxaGFXd2lMQ0p3Y205MmFXUmxjbk1pT2xzaVpXMWhhV3dpWFgwc0luVnpaWEpmYldWMFlXUmhkR0VpT25zaVpXMWhhV3dpT2lKMFpYTjBRSFJsYzNRdVkyOXRJaXdpWlcxaGFXeGZkbVZ5YVdacFpXUWlPbVpoYkhObExDSndhRzl1WlY5MlpYSnBabWxsWkNJNlptRnNjMlVzSW5OMVlpSTZJbU14T1RZNVkyUm1MV1F4TkRndE5ERXhaQzA0TnpVekxXSmxZakF3WldGa05ETmhOeUo5TENKeWIyeGxJam9pWVhWMGFHVnVkR2xqWVhSbFpDSXNJbUZoYkNJNkltRmhiREVpTENKaGJYSWlPbHQ3SW0xbGRHaHZaQ0k2SW5CaGMzTjNiM0prSWl3aWRHbHRaWE4wWVcxd0lqb3hOek16TXpnNE5UWXdmVjBzSW5ObGMzTnBiMjVmYVdRaU9pSmxZV1ptTlRNM05DMHhZVGN4TFRRd1ltVXRZV00wTmkxbVl6WXdaalJtTVdNM05HRWlMQ0pwYzE5aGJtOXVlVzF2ZFhNaU9tWmhiSE5sZlEuUnFtZzdwTUx5c2I5LXplQ2paNFJNdElYVEFoTHVoUkFlNVhhRjBMY0tIayIsInRva2VuX3R5cGUiOiJiZWFyZXIiLCJleHBpcmVzX2luIjozNjAwLCJleHBpcmVzX2F0IjoxNzMzMzkyMTYwLCJyZWZyZXNoX3Rva2VuIjoiemlOS3JybmFveldvMnRMLVQyMWpfUSIsInVzZXIiOnsiaWQiOiJjMTk2OWNkZi1kMTQ4LTQxMWQtODc1My1iZWIwMGVhZDQzYTciLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJlbWFpbF9jb25maXJtZWRfYXQiOiIyMDI0LTExLTEzVDEyOjI3OjMxLjc5MDY0M1oiLCJwaG9uZSI6IiIsImNvbmZpcm1lZF9hdCI6IjIwMjQtMTEtMTNUMTI6Mjc6MzEuNzkwNjQzWiIsImxhc3Rfc2lnbl9pbl9hdCI6IjIwMjQtMTItMDVUMDg6NDk6MjAuNTcxOTQ0MjVaIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZW1haWwiLCJwcm92aWRlcnMiOlsiZW1haWwiXX0sInVzZXJfbWV0YWRhdGEiOnsiZW1haWwiOiJ0ZXN0QHRlc3QuY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwaG9uZV92ZXJpZmllZCI6ZmFsc2UsInN1YiI6ImMxOTY5Y2RmLWQxNDgtNDExZC04NzUzLWJlYjAwZWFkNDNhNyJ9LCJpZGVudGl0aWVzIjpbeyJpZGVudGl0eV9pZCI6ImI4NjRmNjE0LTEwMzctNGE2Yy05NWU1LWQ0NmIyYzUwMjU5MSIsImlkIjoiYzE5NjljZGYtZDE0OC00MTFkLTg3NTMtYmViMDBlYWQ0M2E3IiwidXNlcl9pZCI6ImMxOTY5Y2RmLWQxNDgtNDExZC04NzUzLWJlYjAwZWFkNDNhNyIsImlkZW50aXR5X2RhdGEiOnsiZW1haWwiOiJ0ZXN0QHRlc3QuY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwaG9uZV92ZXJpZmllZCI6ZmFsc2UsInN1YiI6ImMxOTY5Y2RmLWQxNDgtNDExZC04NzUzLWJlYjAwZWFkNDNhNyJ9LCJwcm92aWRlciI6ImVtYWlsIiwibGFzdF9zaWduX2luX2F0IjoiMjAyNC0xMS0xM1QxMjoyNzozMS43ODI3ODVaIiwiY3JlYXRlZF9hdCI6IjIwMjQtMTEtMTNUMTI6Mjc6MzEuNzgyODM5WiIsInVwZGF0ZWRfYXQiOiIyMDI0LTExLTEzVDEyOjI3OjMxLjc4MjgzOVoiLCJlbWFpbCI6InRlc3RAdGVzdC5jb20ifV0sImNyZWF0ZWRfYXQiOiIyMDI0LTExLTEzVDEyOjI3OjMxLjc2NTg0OVoiLCJ1cGRhdGVkX2F0IjoiMjAyNC0xMi0wNVQwODo0OToyMC41NzYxMjdaIiwiaXNfYW5vbnltb3VzIjpmYWxzZX19", - "domain": "localhost", - "path": "/", - "expires": 1767948560.722993, - "httpOnly": false, - "secure": false, - "sameSite": "Lax" - } - ], - "origins": [] -} \ No newline at end of file diff --git a/tests/e2e/adminLayout.spec.ts b/tests/e2e/adminLayout.spec.ts new file mode 100644 index 0000000..38ee97d --- /dev/null +++ b/tests/e2e/adminLayout.spec.ts @@ -0,0 +1,23 @@ +import test from '../fixtures/pageFixtrue' + +test.describe('admin layout common functions', () => { + test('should toggle light theme', async ({ adminLayout }) => { + await adminLayout.navigateToRadomPage() + await adminLayout.toggleLightTheme() + }) + + test('should toggle dark theme', async ({ adminLayout }) => { + await adminLayout.navigateToRadomPage() + await adminLayout.toggleDarkTheme() + }) + + test('should toggle system with light theme', async ({ adminLayout }) => { + await adminLayout.navigateToRadomPage() + await adminLayout.toggleSystemWithLightTheme() + }) + + test('should toggle system with dark theme', async ({ adminLayout }) => { + await adminLayout.navigateToRadomPage() + await adminLayout.toggleSystemWithDarkTheme() + }) +}) diff --git a/tests/e2e/authPage.spec.ts b/tests/e2e/authPage.spec.ts index 7effbfc..537af1e 100644 --- a/tests/e2e/authPage.spec.ts +++ b/tests/e2e/authPage.spec.ts @@ -1,11 +1,17 @@ import test from '../fixtures/pageFixtrue' test.describe('Auth Page', () => { + // 表单验证错误信息验证 + test('login with form validation error', async ({ authPage }) => { + await authPage.navigate() + await authPage.loginWithFormValidationError() + }) + // 登录成功验证 test('should be able to login', async ({ authPage }) => { await authPage.navigate() await authPage.login('test@test.com', '123456') }) - + // 登录失败验证 test('should not be able to login with invalid credentials', async ({ authPage, }) => { diff --git a/tests/e2e/categoriesPage.spec.ts b/tests/e2e/categoriesPage.spec.ts index 158b5dc..299f47e 100644 --- a/tests/e2e/categoriesPage.spec.ts +++ b/tests/e2e/categoriesPage.spec.ts @@ -17,6 +17,11 @@ test.describe('Categories Page', () => { await categoriesPage.navigateToProducts() }) + test('form validation error messages', async ({ categoriesPage }) => { + await categoriesPage.navigate() + await categoriesPage.formvalidationErrorMessages() + }) + test('add new category', async ({ categoriesPage }) => { const categoryName = faker.commerce.productName() await categoriesPage.navigate() diff --git a/tests/e2e/dashboardPage.spec.ts b/tests/e2e/dashboardPage.spec.ts index 377ea4e..c51803d 100644 --- a/tests/e2e/dashboardPage.spec.ts +++ b/tests/e2e/dashboardPage.spec.ts @@ -1,31 +1,31 @@ import test from '../fixtures/pageFixtrue' test.describe('Dashboard Page', () => { - test('ordersChartCheck', async ({ dashboardPage }) => { + test('ordersc chart check', async ({ dashboardPage }) => { await dashboardPage.navigate() await dashboardPage.ordersChartCheck() }) - test('productDistributionCheck', async ({ dashboardPage }) => { + test('product distribution chart check', async ({ dashboardPage }) => { await dashboardPage.navigate() - await dashboardPage.productDistributionCheck() + await dashboardPage.productDistributionChartCheck() }) - test('productsPerCategoryChartCheck', async ({ dashboardPage }) => { + test('products per category chart check', async ({ dashboardPage }) => { await dashboardPage.navigate() await dashboardPage.productsPerCategoryChartCheck() }) - test('latestUsersChartCheck', async ({ dashboardPage }) => { + test('latestUsers chart check', async ({ dashboardPage }) => { await dashboardPage.navigate() await dashboardPage.latestUsersChartCheck() }) - test('navigateToOrdersPage', async ({ dashboardPage }) => { + test('navigate to orders page', async ({ dashboardPage }) => { await dashboardPage.navigate() await dashboardPage.navigateToOrdersPage() }) - test('navigateToProductsPage', async ({ dashboardPage }) => { + test('navigate to products page', async ({ dashboardPage }) => { await dashboardPage.navigate() await dashboardPage.navigateToProductsPage() }) - test('navigateToCategoriesPage', async ({ dashboardPage }) => { + test('navigate to categories page', async ({ dashboardPage }) => { await dashboardPage.navigate() await dashboardPage.navigateToCategoriesPage() }) diff --git a/tests/e2e/logout.spec.ts b/tests/e2e/logout.spec.ts new file mode 100644 index 0000000..f2d06db --- /dev/null +++ b/tests/e2e/logout.spec.ts @@ -0,0 +1,25 @@ +import test from '@playwright/test' +import { AdminLayout } from '../pages/adminLayout' +import { AuthPage } from '../pages/authPage' + +test.describe('Logout functionality', () => { + test('Logout', async ({ browser }) => { + const context = await browser.newContext({ + storageState: 'tests/.auth/logout.json', // 使用 user 的状态文件 + }) + const page = await context.newPage() + + const authPage = new AuthPage(page) + await authPage.navigate() + await authPage.login('test@test.com', '123456') + + // 测试登出逻辑 + const adminLayout = new AdminLayout(page) + await adminLayout.navigateToRadomPage() // 跳转到随机页面 + await adminLayout.logout() + + // 验证已退出(比如应该重定向到登录页) + + await context.close() + }) +}) diff --git a/tests/e2e/productsPage.spec.ts b/tests/e2e/productsPage.spec.ts index 6501d0d..fff8654 100644 --- a/tests/e2e/productsPage.spec.ts +++ b/tests/e2e/productsPage.spec.ts @@ -2,21 +2,26 @@ import test from '../fixtures/pageFixtrue' import { faker } from '@faker-js/faker' test.describe('Products Page', () => { - test('navigateToDashboard', async ({ productsPage }) => { + test('navigate to dashboard page', async ({ productsPage }) => { await productsPage.navigate() await productsPage.navigateToDashboard() }) - test('navigateToOrdersPage', async ({ productsPage }) => { + test('navigate to Orders page', async ({ productsPage }) => { await productsPage.navigate() await productsPage.navigateToOrdersPage() }) - test('navigateToCategoriesPage', async ({ productsPage }) => { + test('navigate to categories page', async ({ productsPage }) => { await productsPage.navigate() await productsPage.navigateToCategoriesPage() }) + test('form validation error message', async ({ productsPage }) => { + await productsPage.navigate() + await productsPage.formValidationErrorMessages() + }) + test('add new product', async ({ productsPage }) => { // 生成一个随机产品名称 const productTitle = faker.commerce.productName() diff --git a/tests/fixtures/pageFixtrue.ts b/tests/fixtures/pageFixtrue.ts index c0ea46a..b777da4 100644 --- a/tests/fixtures/pageFixtrue.ts +++ b/tests/fixtures/pageFixtrue.ts @@ -5,6 +5,7 @@ import { DashboardPage } from '../pages/dashboardPage' import { OrdersPage } from '../pages/ordersPage' import { ProductsPage } from '../pages/productsPage' import { CategoriesPage } from '../pages/categoriesPage' +import { AdminLayout } from '../pages/adminLayout' const test = baseTest.extend<{ homePage: HomePage @@ -13,6 +14,7 @@ const test = baseTest.extend<{ ordersPage: OrdersPage productsPage: ProductsPage categoriesPage: CategoriesPage + adminLayout: AdminLayout }>({ homePage: async ({ page }, use) => { await use(new HomePage(page)) @@ -20,6 +22,9 @@ const test = baseTest.extend<{ authPage: async ({ page }, use) => { await use(new AuthPage(page)) }, + adminLayout: async ({ page }, use) => { + await use(new AdminLayout(page)) + }, dashboardPage: async ({ page }, use) => { await use(new DashboardPage(page)) }, diff --git a/tests/pages/adminLayout.ts b/tests/pages/adminLayout.ts new file mode 100644 index 0000000..aedd799 --- /dev/null +++ b/tests/pages/adminLayout.ts @@ -0,0 +1,78 @@ +import { expect, Page } from '@playwright/test' +import { HelperBase } from './helperBase' +import { getRandomElement } from '../lib/utils' + +export class AdminLayout extends HelperBase { + readonly allLinksInAdminLayout = [ + '/admin/dashboard', + '/admin/orders', + '/admin/products', + '/admin/categories', + ] + readonly userProfileButton = this.page.getByRole('button', { + name: 'Toggle user menu', + }) + readonly logoutButton = this.page.getByRole('menuitem', { name: 'Logout' }) + readonly toggleThemeButton = this.page.getByRole('button', { + name: 'Toggle theme', + }) + readonly lightThemeButton = this.page.getByRole('menuitem', { + name: 'Light', + }) + readonly darkThemeButton = this.page.getByRole('menuitem', { + name: 'Dark', + }) + readonly systemThemeButton = this.page.getByRole('menuitem', { + name: 'System', + }) + + constructor(page: Page) { + super(page) + } + + async navigateToRadomPage() { + const randomLink = getRandomElement(this.allLinksInAdminLayout) + await this.page.goto(randomLink as string) + } + + async logout() { + await this.userProfileButton.click() + await this.logoutButton.click() + await this.page.waitForURL('/') + await expect( + this.page.locator('h1', { hasText: 'GadgetApp' }) + ).toBeVisible() + } + + async toggleLightTheme() { + await this.userProfileButton.click() + await this.toggleThemeButton.click() + await this.lightThemeButton.click() + await expect(this.page.locator('html')).toHaveClass('light') + } + + async toggleDarkTheme() { + await this.userProfileButton.click() + await this.toggleThemeButton.click() + await this.darkThemeButton.click() + await expect(this.page.locator('html')).toHaveClass('dark') + } + + async toggleSystemWithLightTheme() { + // 模拟系统主题为 Light + await this.page.emulateMedia({ colorScheme: 'light' }) + await this.userProfileButton.click() + await this.toggleThemeButton.click() + await this.systemThemeButton.click() + await expect(this.page.locator('html')).toHaveClass('light') + } + + async toggleSystemWithDarkTheme() { + // 模拟系统主题为 Dark + await this.page.emulateMedia({ colorScheme: 'dark' }) + await this.userProfileButton.click() + await this.toggleThemeButton.click() + await this.systemThemeButton.click() + await expect(this.page.locator('html')).toHaveClass('dark') + } +} diff --git a/tests/pages/authPage.ts b/tests/pages/authPage.ts index 62ebdc6..b6715eb 100644 --- a/tests/pages/authPage.ts +++ b/tests/pages/authPage.ts @@ -7,6 +7,12 @@ export class AuthPage extends HelperBase { readonly loginButton = this.page.getByText('Login') readonly successMessage = this.page.getByText('Logged in successfully') readonly errorMessage = this.page.getByText('Invalid email or password') + readonly formValidationEmailError = this.page.getByText( + 'Invalid email address' + ) + readonly formValidationPasswordError = this.page.getByText( + 'Password must be at least 6 characters' + ) constructor(page: Page) { super(page) @@ -33,6 +39,12 @@ export class AuthPage extends HelperBase { await expect(dashboardHeader).toBeVisible() } + async loginWithFormValidationError() { + await this.loginButton.click() + await expect(this.formValidationEmailError).toBeVisible() + await expect(this.formValidationPasswordError).toBeVisible() + } + async loginWithError(email: string, password: string) { await this.email.fill(email) await this.password.fill(password) diff --git a/tests/pages/categoriesPage.ts b/tests/pages/categoriesPage.ts index fc5e447..753b408 100644 --- a/tests/pages/categoriesPage.ts +++ b/tests/pages/categoriesPage.ts @@ -17,6 +17,10 @@ export class CategoriesPage extends HelperBase { name: 'Add Category', }) + // 表单验证提示 + readonly formValidationNameMessage = this.page.getByText('分类名称是必填项') + readonly formValidationImageMessage = this.page.getByText('Invalid url') + // 分类名称输入框 readonly categoryNameInput = this.page.getByLabel('Name') @@ -76,6 +80,13 @@ export class CategoriesPage extends HelperBase { await expect(this.page.getByText('Products Management')).toBeVisible() } + async formvalidationErrorMessages() { + await this.addCategoryButton.click() + await this.submitButton.click() + await expect(this.formValidationNameMessage).toBeVisible() + await expect(this.formValidationImageMessage).toBeVisible() + } + async addCategory(name: string) { await this.addCategoryButton.click() await this.categoryNameInput.fill(name) diff --git a/tests/pages/dashboardPage.ts b/tests/pages/dashboardPage.ts index f340edd..601d1ab 100644 --- a/tests/pages/dashboardPage.ts +++ b/tests/pages/dashboardPage.ts @@ -3,7 +3,7 @@ import { HelperBase } from './helperBase' export class DashboardPage extends HelperBase { readonly ordersChartTitle = this.page.getByText('Orders Over Time') - readonly productDistributionTitle = this.page.getByText( + readonly productDistributionChartTitle = this.page.getByText( 'Product Distribution' ) readonly productsPerCategoryChartTitle = this.page.getByText( @@ -32,8 +32,8 @@ export class DashboardPage extends HelperBase { await expect(this.ordersChartTitle).toBeVisible() } - async productDistributionCheck() { - await expect(this.productDistributionTitle).toBeVisible() + async productDistributionChartCheck() { + await expect(this.productDistributionChartTitle).toBeVisible() } async productsPerCategoryChartCheck() { diff --git a/tests/pages/productsPage.ts b/tests/pages/productsPage.ts index e6e108a..97032b8 100644 --- a/tests/pages/productsPage.ts +++ b/tests/pages/productsPage.ts @@ -3,6 +3,7 @@ import { HelperBase } from './helperBase' import path from 'path' export class ProductsPage extends HelperBase { + // 导航链接 readonly dashboardPageLink = this.page.getByRole('link', { name: 'Dashboard', }) @@ -12,6 +13,21 @@ export class ProductsPage extends HelperBase { readonly categoriesPageLink = this.page.getByRole('link', { name: 'Categories', }) + + // 表单验证提示信息 + readonly titleRequiredMessage = this.page.getByText('Title is required') + readonly categoryRequiredMessage = this.page.getByText('Category is required') + readonly priceRequiredMessage = this.page.getByText('Price is required') + readonly maxQuantityRequiredMessage = this.page.getByText( + 'Max Quantity is required' + ) + readonly heroImageRequiredMessage = this.page.getByText( + 'Hero Image is required' + ) + readonly productsImageRequiredMessage = this.page.getByText( + 'At least one image is required' + ) + // 添加产品按钮 readonly addProductButton = this.page.getByRole('button', { name: 'Add Product', @@ -78,6 +94,17 @@ export class ProductsPage extends HelperBase { ).toBeVisible() } + async formValidationErrorMessages() { + await this.addProductButton.click() + await this.submitButton.click() + await expect(this.titleRequiredMessage).toBeVisible() + await expect(this.categoryRequiredMessage).toBeVisible() + await expect(this.priceRequiredMessage).toBeVisible() + await expect(this.maxQuantityRequiredMessage).toBeVisible() + await expect(this.heroImageRequiredMessage).toBeVisible() + await expect(this.productsImageRequiredMessage).toBeVisible() + } + async addProduct( title: string, category: string, diff --git a/tests/setup/setup-admin.setup.ts b/tests/setup/setup-admin.setup.ts new file mode 100644 index 0000000..add2467 --- /dev/null +++ b/tests/setup/setup-admin.setup.ts @@ -0,0 +1,24 @@ +import { test as setup } from '@playwright/test' +import path from 'path' + +const authFile = path.join(__dirname, '../.auth/admin.json') + +setup('authenticate', async ({ page }) => { + // Perform authentication steps. Replace these actions with your own. + await page.goto('/auth') + await page.getByLabel('Email').fill('test@admin.com') + await page.getByLabel('Password').fill('123456') + await page.getByText('Login').click() + // Wait until the page receives the cookies. + // + // Sometimes login flow sets cookies in the process of several redirects. + // Wait for the final URL to ensure that the cookies are actually set. + await page.waitForURL('http://localhost:3000/admin/dashboard', { + waitUntil: 'load', + }) + // Alternatively, you can wait until the page reaches a state where all cookies are set. + + // End of authentication steps. + + await page.context().storageState({ path: authFile }) +}) diff --git a/tests/setup/auth.setup.ts b/tests/setup/setup-user.setup.ts similarity index 93% rename from tests/setup/auth.setup.ts rename to tests/setup/setup-user.setup.ts index 72364b5..f4a31f0 100644 --- a/tests/setup/auth.setup.ts +++ b/tests/setup/setup-user.setup.ts @@ -6,7 +6,7 @@ const authFile = path.join(__dirname, '../.auth/user.json') setup('authenticate', async ({ page }) => { // Perform authentication steps. Replace these actions with your own. await page.goto('/auth') - await page.getByLabel('Email').fill('test@test.com') + await page.getByLabel('Email').fill('test@user.com') await page.getByLabel('Password').fill('123456') await page.getByText('Login').click() // Wait until the page receives the cookies.