diff --git a/backend/LexBoxApi/appsettings.Development.json b/backend/LexBoxApi/appsettings.Development.json index 995c9d35a..35f3ab4dc 100644 --- a/backend/LexBoxApi/appsettings.Development.json +++ b/backend/LexBoxApi/appsettings.Development.json @@ -38,7 +38,7 @@ "CloudFlare": { // always passes key, more info here: https://developers.cloudflare.com/turnstile/frequently-asked-questions/#are-there-sitekeys-and-secret-keys-that-can-be-used-for-testing "TurnstileKey": "1x0000000000000000000000000000000AA", - "AllowDomain": "mailinator.com" + "AllowDomain": "maildev.com" }, "HgConfig": { "RepoPath": "../../hgweb/repos", diff --git a/deployment/develop/lexbox-deployment.patch.yaml b/deployment/develop/lexbox-deployment.patch.yaml index 0e1db473d..582d87f7e 100644 --- a/deployment/develop/lexbox-deployment.patch.yaml +++ b/deployment/develop/lexbox-deployment.patch.yaml @@ -12,7 +12,7 @@ spec: - name: lexbox-api env: - name: CloudFlare__AllowDomain - value: "mailinator.com" + value: "developermail.com" - name: ASPNETCORE_ENVIRONMENT value: "Staging" #we don't want to act like dev as that's for local development valueFrom: diff --git a/deployment/local-dev/lexbox-deployment.patch.yaml b/deployment/local-dev/lexbox-deployment.patch.yaml index 9d2f83656..08ebb18ef 100644 --- a/deployment/local-dev/lexbox-deployment.patch.yaml +++ b/deployment/local-dev/lexbox-deployment.patch.yaml @@ -35,7 +35,7 @@ spec: value: "1x0000000000000000000000000000000AA" valueFrom: - name: CloudFlare__AllowDomain - value: "mailinator.com" + value: "maildev.com" - name: HealthChecksConfig__RequireFwHeadlessContainerVersionMatch value: "false" - name: HealthChecksConfig__RequireHealthyFwHeadlessContainer diff --git a/deployment/staging/lexbox-deployment.patch.yaml b/deployment/staging/lexbox-deployment.patch.yaml index 123bbdada..c72962881 100644 --- a/deployment/staging/lexbox-deployment.patch.yaml +++ b/deployment/staging/lexbox-deployment.patch.yaml @@ -18,7 +18,7 @@ spec: env: - name: CloudFlare__AllowDomain - value: "mailinator.com" + value: "developermail.com" - name: Email__SmtpHost value: email-smtp.us-east-1.amazonaws.com - name: Email__SmtpPort diff --git a/frontend/.npmrc b/frontend/.npmrc index 8153bffaf..366dd5c83 100644 --- a/frontend/.npmrc +++ b/frontend/.npmrc @@ -1,2 +1,3 @@ engine-strict=true include-workspace-root=true +ignore-workspace-root-check=true # we commonly add packages to the root package.json (aka frontend/package.json) diff --git a/frontend/package.json b/frontend/package.json index 94a988851..302d078e0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -95,6 +95,7 @@ "@types/set-cookie-parser": "^2.4.7", "@vitejs/plugin-basic-ssl": "^1.1.0", "css-tree": "^2.3.1", + "e2e-mailbox": "1.1.5", "js-cookie": "^3.0.5", "just-order-by": "^1.0.0", "mjml": "^4.15.3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2628def54..30589ae61 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: css-tree: specifier: ^2.3.1 version: 2.3.1 + e2e-mailbox: + specifier: 1.1.5 + version: 1.1.5 js-cookie: specifier: ^3.0.5 version: 3.0.5 @@ -2138,6 +2141,9 @@ packages: cpu: [x64] os: [win32] + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@sveltejs/adapter-node@4.0.1': resolution: {integrity: sha512-IviiTtKCDp+0QoTmmMlGGZBA1EoUNsjecU6XGV9k62S3f01SNsVhpqi2e4nbI62BLGKh/YKKfFii+Vz/b9XIxg==} peerDependencies: @@ -2637,6 +2643,9 @@ packages: async@3.2.5: resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomically@1.7.0: resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} engines: {node: '>=10.12.0'} @@ -2652,6 +2661,9 @@ packages: peerDependencies: postcss: ^8.1.0 + axios@1.7.4: + resolution: {integrity: sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==} + axobject-query@4.0.0: resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==} @@ -2900,6 +2912,10 @@ packages: combine-errors@3.0.3: resolution: {integrity: sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -3101,6 +3117,10 @@ packages: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dependency-graph@0.11.0: resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} engines: {node: '>= 0.6.0'} @@ -3195,6 +3215,9 @@ packages: resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==} engines: {node: '>=4'} + e2e-mailbox@1.1.5: + resolution: {integrity: sha512-te+CSEbba3eHzYZNopYF8M31yYuE33k/28+6BZ7mhTNQ92PsjByAHEPiWsL/U2NEy5NLWbWR7rF2o7dH4GRX/Q==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -3221,6 +3244,14 @@ packages: enabled@2.0.0: resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + encoding-japanese@2.0.0: + resolution: {integrity: sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==} + engines: {node: '>=8.10.0'} + + encoding-japanese@2.1.0: + resolution: {integrity: sha512-58XySVxUgVlBikBTbQ8WdDxBDHIdXucB16LO5PBHR8t75D54wQrNo4cg+58+R1CtJfKnsVsvt9XlteRaR8xw1w==} + engines: {node: '>=8.10.0'} + entities@2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} @@ -3452,10 +3483,23 @@ packages: fn.name@1.1.0: resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -3617,6 +3661,10 @@ packages: engines: {node: '>=6'} hasBin: true + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + htmlparser2@5.0.1: resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==} @@ -3642,6 +3690,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -3926,10 +3978,31 @@ packages: kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libbase64@1.2.1: + resolution: {integrity: sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==} + + libbase64@1.3.0: + resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==} + + libmime@5.2.0: + resolution: {integrity: sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw==} + + libmime@5.3.5: + resolution: {integrity: sha512-nSlR1yRZ43L3cZCiWEw7ali3jY29Hz9CQQ96Oy+sSspYnIP5N54ucOPHqooBsXzwrX1pwn13VUE05q4WmzfaLg==} + + libqp@2.0.1: + resolution: {integrity: sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg==} + + libqp@2.1.0: + resolution: {integrity: sha512-O6O6/fsG5jiUVbvdgT7YX3xY3uIadR6wEZ7+vy9u7PKHAlSEB6blvC1o5pHBjgsi95Uo0aiBBdkyFecj6jtb7A==} + lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -3945,6 +4018,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + listr2@4.0.5: resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} engines: {node: '>=12'} @@ -4076,6 +4152,12 @@ packages: magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} + mailparser@3.7.1: + resolution: {integrity: sha512-RCnBhy5q8XtB3mXzxcAfT1huNqN93HTYYyL6XawlIKycfxM/rXPg9tXoZ7D46+SgCS1zxKzw+BayDQSvncSTTw==} + + mailsplit@5.4.0: + resolution: {integrity: sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA==} + map-cache@0.2.2: resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} engines: {node: '>=0.10.0'} @@ -4229,6 +4311,14 @@ packages: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} engines: {node: '>=4.0.0'} @@ -4433,6 +4523,10 @@ packages: node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + nodemailer@6.9.13: + resolution: {integrity: sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==} + engines: {node: '>=6.0.0'} + nopt@7.2.0: resolution: {integrity: sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -4554,6 +4648,9 @@ packages: parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} @@ -4606,6 +4703,9 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} @@ -4810,9 +4910,16 @@ packages: resolution: {integrity: sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==} engines: {node: '>=12.0.0'} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} @@ -4998,6 +5105,9 @@ packages: scuid@1.1.0: resolution: {integrity: sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==} + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -5333,6 +5443,10 @@ packages: title-case@3.0.3: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} + tlds@1.252.0: + resolution: {integrity: sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==} + hasBin: true + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -5431,6 +5545,9 @@ packages: ua-parser-js@1.0.37: resolution: {integrity: sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==} + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.3.2: resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==} @@ -8052,6 +8169,11 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.9.6': optional: true + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + '@sveltejs/adapter-node@4.0.1(@sveltejs/kit@2.5.10(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.9(@types/node@20.12.12)))(svelte@4.2.19)(vite@5.4.9(@types/node@20.12.12)))': dependencies: '@rollup/plugin-commonjs': 25.0.7(rollup@4.9.6) @@ -8681,6 +8803,8 @@ snapshots: async@3.2.5: {} + asynckit@0.4.0: {} + atomically@1.7.0: {} auto-bind@4.0.0: {} @@ -8695,6 +8819,14 @@ snapshots: postcss: 8.4.47 postcss-value-parser: 4.2.0 + axios@1.7.4: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.0.0: dependencies: dequal: 2.0.3 @@ -9026,6 +9158,10 @@ snapshots: custom-error-instance: 2.1.1 lodash.uniqby: 4.5.0 + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@10.0.1: {} commander@11.1.0: {} @@ -9210,6 +9346,8 @@ snapshots: define-lazy-prop@2.0.0: {} + delayed-stream@1.0.0: {} + dependency-graph@0.11.0: {} dequal@2.0.3: {} @@ -9299,6 +9437,13 @@ snapshots: dset@3.1.3: {} + e2e-mailbox@1.1.5: + dependencies: + axios: 1.7.4 + mailparser: 3.7.1 + transitivePeerDependencies: + - debug + eastasianwidth@0.2.0: {} editorconfig@1.0.4: @@ -9321,6 +9466,10 @@ snapshots: enabled@2.0.0: {} + encoding-japanese@2.0.0: {} + + encoding-japanese@2.1.0: {} + entities@2.2.0: {} entities@4.5.0: {} @@ -9605,11 +9754,19 @@ snapshots: fn.name@1.1.0: {} + follow-redirects@1.15.9: {} + foreground-child@3.1.1: dependencies: cross-spawn: 7.0.3 signal-exit: 4.1.0 + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + fraction.js@4.3.7: {} fs.realpath@1.0.0: {} @@ -9792,6 +9949,14 @@ snapshots: relateurl: 0.2.7 uglify-js: 3.17.4 + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + htmlparser2@5.0.1: dependencies: domelementtype: 2.3.0 @@ -9833,6 +9998,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.1: {} @@ -10082,11 +10251,35 @@ snapshots: kuler@2.0.0: {} + leac@0.6.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 + libbase64@1.2.1: {} + + libbase64@1.3.0: {} + + libmime@5.2.0: + dependencies: + encoding-japanese: 2.0.0 + iconv-lite: 0.6.3 + libbase64: 1.2.1 + libqp: 2.0.1 + + libmime@5.3.5: + dependencies: + encoding-japanese: 2.1.0 + iconv-lite: 0.6.3 + libbase64: 1.3.0 + libqp: 2.1.0 + + libqp@2.0.1: {} + + libqp@2.1.0: {} + lilconfig@2.1.0: {} lilconfig@3.0.0: {} @@ -10096,6 +10289,10 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + listr2@4.0.5: dependencies: cli-truncate: 2.1.0 @@ -10230,6 +10427,25 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + mailparser@3.7.1: + dependencies: + encoding-japanese: 2.1.0 + he: 1.2.0 + html-to-text: 9.0.5 + iconv-lite: 0.6.3 + libmime: 5.3.5 + linkify-it: 5.0.0 + mailsplit: 5.4.0 + nodemailer: 6.9.13 + punycode.js: 2.3.1 + tlds: 1.252.0 + + mailsplit@5.4.0: + dependencies: + libbase64: 1.2.1 + libmime: 5.2.0 + libqp: 2.0.1 + map-cache@0.2.2: {} markdown-table@3.0.3: {} @@ -10555,6 +10771,12 @@ snapshots: braces: 3.0.2 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mime@2.6.0: {} mimic-fn@2.1.0: {} @@ -10940,6 +11162,8 @@ snapshots: node-releases@2.0.18: optional: true + nodemailer@6.9.13: {} + nopt@7.2.0: dependencies: abbrev: 2.0.0 @@ -11072,6 +11296,11 @@ snapshots: dependencies: entities: 4.5.0 + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + pascal-case@3.1.2: dependencies: no-case: 3.0.4 @@ -11111,6 +11340,8 @@ snapshots: pathval@2.0.0: {} + peberminta@0.9.0: {} + periscopic@3.1.0: dependencies: '@types/estree': 1.0.5 @@ -11302,8 +11533,12 @@ snapshots: '@types/node': 20.12.12 long: 5.2.3 + proxy-from-env@1.1.0: {} + psl@1.9.0: {} + punycode.js@2.3.1: {} + punycode@1.4.1: {} punycode@2.3.1: {} @@ -11516,6 +11751,10 @@ snapshots: scuid@1.1.0: {} + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + semver@6.3.1: {} semver@7.5.4: @@ -11923,6 +12162,8 @@ snapshots: dependencies: tslib: 2.6.2 + tlds@1.252.0: {} + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -12001,6 +12242,8 @@ snapshots: ua-parser-js@1.0.37: {} + uc.micro@2.1.0: {} + ufo@1.3.2: {} ufo@1.5.3: {} diff --git a/frontend/tests/email/e2e-mailbox-module-patched.ts b/frontend/tests/email/e2e-mailbox-module-patched.ts new file mode 100644 index 000000000..91e857e79 --- /dev/null +++ b/frontend/tests/email/e2e-mailbox-module-patched.ts @@ -0,0 +1,11 @@ +import E2EMailboxModuleConfused from 'e2e-mailbox'; + +// The package seems to think it's an ES module, but it's actually a commonJS module. +// https://github.com/allynsweet/E2E-Mailbox/issues/5 + +// eslint-disable-next-line @typescript-eslint/naming-convention +const E2EMailboxClass = (E2EMailboxModuleConfused as unknown as { + default: typeof E2EMailboxModuleConfused +}).default; + +export class E2EMailboxApi extends E2EMailboxClass { } diff --git a/frontend/tests/email/e2e-mailbox.ts b/frontend/tests/email/e2e-mailbox.ts new file mode 100644 index 000000000..dd8364960 --- /dev/null +++ b/frontend/tests/email/e2e-mailbox.ts @@ -0,0 +1,20 @@ +import {Mailbox, type Email} from './mailbox'; + +import type {E2EMailboxApi} from './e2e-mailbox-module-patched'; +import type {EmailSubjects} from './email-page'; + +export class E2EMailbox extends Mailbox { + + constructor( + readonly email: string, + private readonly e2eMailboxApi: E2EMailboxApi, + ) { + super(email); + } + + async fetchEmails(subject: EmailSubjects): Promise { + const emails = await this.e2eMailboxApi.fetchEmailList() + return emails.filter(email => email.mail_subject.includes(subject)) + .map(email => ({body: email.mail_body})); + } +} diff --git a/frontend/tests/email/email-page.ts b/frontend/tests/email/email-page.ts new file mode 100644 index 000000000..bd0dac960 --- /dev/null +++ b/frontend/tests/email/email-page.ts @@ -0,0 +1,36 @@ +import {type Locator, type Page} from '@playwright/test'; +import {BasePage} from '../pages/basePage'; +import {serverBaseUrl} from '../envVars'; + +export enum EmailSubjects { + VerifyEmail = 'Verify your e-mail address', + ForgotPassword = 'Forgot your password?', + PasswordChanged = 'Your password was changed', + ProjectInvitation = 'Project invitation:', +} + +export class EmailPage extends BasePage { + readonly bodyLocator: Locator; + public get resetPasswordButton(): Locator { return this.bodyLocator.getByRole('link', {name: 'Reset password'}); } + + constructor(page: Page, url?: string) { + super(page, page.locator('body', {has: page.locator('a[href*="https://lexbox.org"]').first()}), url); + this.bodyLocator = this.locators[0]; + } + + clickVerifyEmail(): Promise { + return this.bodyLocator.getByRole('link', {name: 'Verify e-mail'}).click(); + } + + clickResetPassword(): Promise { + return this.resetPasswordButton.click(); + } + + getFirstLanguageDepotUrl(): Promise { + return this.bodyLocator.locator(`a[href*='${serverBaseUrl}']`).first().getAttribute('href'); + } + + clickFirstLanguageDepotUrl(): Promise { + return this.bodyLocator.locator(`a[href*='${serverBaseUrl}']`).first().click(); + } +} diff --git a/frontend/tests/email/mailbox.ts b/frontend/tests/email/mailbox.ts new file mode 100644 index 000000000..f256fa9ed --- /dev/null +++ b/frontend/tests/email/mailbox.ts @@ -0,0 +1,30 @@ +import {type EmailSubjects, EmailPage} from './email-page'; + +import {expect, type Page} from '@playwright/test'; + +export interface Email { + body: string; +} + +export abstract class Mailbox { + constructor(readonly email: string) { } + + abstract fetchEmails(subject: EmailSubjects): Promise; + + async openEmail(page: Page, subject: EmailSubjects, index: number = 0): Promise { + let email: Email | undefined = undefined; + + await expect.poll(async () => { + const emails = await this.fetchEmails(subject); + email = emails[index]; + return email; + }, { + intervals: [1_000], + timeout: 10_000, + message: `Failed to find email: ${subject}. (Index: ${index})`, + }).toBeDefined(); + + await page.setContent(email!.body); + return await new EmailPage(page).waitFor(); + } +} diff --git a/frontend/tests/email/maildev-mailbox.ts b/frontend/tests/email/maildev-mailbox.ts new file mode 100644 index 000000000..c48650d75 --- /dev/null +++ b/frontend/tests/email/maildev-mailbox.ts @@ -0,0 +1,29 @@ +import type {APIRequestContext} from '@playwright/test'; +import {type EmailSubjects} from './email-page'; +import {Mailbox, type Email} from './mailbox'; + +interface MaildevEmail { + subject: string; + html: string; + to: {address: string, name: string}[], + from: {address: string, name: string}[], +} + +export class MaildevMailbox extends Mailbox { + constructor(readonly email: string, private readonly api: APIRequestContext) { + super(email); + } + + async fetchEmails(subject: EmailSubjects): Promise { + const emails = await this.fetchMyEmails(); + return emails.filter(email => email.subject.includes(subject)) + .map(email => ({body: email.html})); + } + + async fetchMyEmails(): Promise { + // Maildev REST API docs: https://github.com/maildev/maildev/blob/master/docs/rest.md + const response = await this.api.get(`http://localhost:1080/email`); + const emails = await response.json() as MaildevEmail[]; + return emails.filter(email => email.to.some(to => to.address === this.email)); + } +} diff --git a/frontend/tests/emailWorkflow.test.ts b/frontend/tests/emailWorkflow.test.ts index 9cb4c2569..988c85934 100644 --- a/frontend/tests/emailWorkflow.test.ts +++ b/frontend/tests/emailWorkflow.test.ts @@ -1,17 +1,16 @@ -import { TEST_TIMEOUT_2X, defaultPassword } from './envVars'; -import { deleteUser, getCurrentUserId, loginAs, logout } from './utils/authHelpers'; - -import { AdminDashboardPage } from './pages/adminDashboardPage'; -import { EmailSubjects } from './pages/mailPages'; -import { LoginPage } from './pages/loginPage'; -import { AcceptInvitationPage } from './pages/acceptInvitationPage'; -import { ResetPasswordPage } from './pages/resetPasswordPage'; -import { UserAccountSettingsPage } from './pages/userAccountSettingsPage'; -import { UserDashboardPage } from './pages/userDashboardPage'; -import { expect } from '@playwright/test'; -import { getInbox } from './utils/mailboxHelpers'; -import { randomUUID } from 'crypto'; -import { test } from './fixtures'; +import {TEST_TIMEOUT_2X, defaultPassword} from './envVars'; +import {deleteUser, getCurrentUserId, loginAs, logout} from './utils/authHelpers'; + +import {AcceptInvitationPage} from './pages/acceptInvitationPage'; +import {AdminDashboardPage} from './pages/adminDashboardPage'; +import {EmailSubjects} from './email/email-page'; +import {LoginPage} from './pages/loginPage'; +import {ResetPasswordPage} from './pages/resetPasswordPage'; +import {UserAccountSettingsPage} from './pages/userAccountSettingsPage'; +import {UserDashboardPage} from './pages/userDashboardPage'; +import {expect} from '@playwright/test'; +import {randomUUID} from 'crypto'; +import {test} from './fixtures'; const userIdsToDelete: string[] = []; @@ -25,7 +24,7 @@ test.afterEach(async ({ page }) => { } }); -test('register, verify, update, verify email address', async ({ page, tempUser }) => { +test('register, verify, update, verify email address', async ({ page, tempUser, mailboxFactory }) => { test.slow(); // Checking email and logging in repeatedly takes time await loginAs(page.request, tempUser.email, tempUser.password); const userDashboardPage = await new UserDashboardPage(page).goto(); @@ -35,8 +34,7 @@ test('register, verify, update, verify email address', async ({ page, tempUser } await userDashboardPage.emailVerificationAlert.clickResendEmail(); await userDashboardPage.emailVerificationAlert.assertVerificationSent(); - const inboxPage = await getInbox(page, tempUser.mailinatorId).goto(); - let emailPage = await inboxPage.openEmail(EmailSubjects.VerifyEmail); + let emailPage = await tempUser.mailbox.openEmail(page, EmailSubjects.VerifyEmail); let pagePromise = emailPage.page.context().waitForEvent('page'); await emailPage.clickVerifyEmail(); let newPage = await pagePromise; @@ -50,17 +48,15 @@ test('register, verify, update, verify email address', async ({ page, tempUser } await userPage.emailVerificationAlert.assertGone(); // Request new email address - const newMailinatorId = randomUUID(); - const newEmail = `${newMailinatorId}@mailinator.com`; + const mailbox = await mailboxFactory(); await userPage.goto(); - await userPage.fillEmail(newEmail); + await userPage.fillEmail(mailbox.email); await userPage.clickSave(); await userPage.emailVerificationAlert.assertRequestedToChange(); // Verify new email address - await inboxPage.gotoMailbox(newMailinatorId); - emailPage = await inboxPage.openEmail(EmailSubjects.VerifyEmail); + emailPage = await mailbox.openEmail(page, EmailSubjects.VerifyEmail); pagePromise = emailPage.page.context().waitForEvent('page'); await emailPage.clickVerifyEmail(); newPage = await pagePromise; @@ -68,9 +64,9 @@ test('register, verify, update, verify email address', async ({ page, tempUser } await userPage.emailVerificationAlert.assertSuccessfullyUpdated(); - // Confirm new meail address works + // Confirm new email address works const loginPage = await logout(page); - await loginPage.fillForm(newEmail, tempUser.password); + await loginPage.fillForm(mailbox.email, tempUser.password); await loginPage.submit(); await userDashboardPage.waitFor(); }); @@ -85,11 +81,10 @@ test('forgot password', async ({ page, tempUser }) => { await page.locator(':text("Check Your Inbox")').first().waitFor(); // Use reset password link - const inboxPage = await getInbox(page, tempUser.mailinatorId).goto(); - const emailPage = await inboxPage.openEmail(EmailSubjects.ForgotPassword); + const emailPage = await tempUser.mailbox.openEmail(page, EmailSubjects.ForgotPassword); const resetPasswordUrl = await emailPage.getFirstLanguageDepotUrl(); expect(resetPasswordUrl).not.toBeNull(); - expect(resetPasswordUrl!).toContain('resetPassword'); + expect(resetPasswordUrl).toContain('resetPassword'); const pagePromise = emailPage.page.context().waitForEvent('page'); await emailPage.clickResetPassword(); @@ -112,7 +107,7 @@ test('forgot password', async ({ page, tempUser }) => { await expect(loginPage.page.getByText('The email you clicked has expired')).toBeVisible(); }); -test('register via new-user invitation email', async ({ page }) => { +test('register via new-user invitation email', async ({ page, mailboxFactory }) => { test.setTimeout(TEST_TIMEOUT_2X); await loginAs(page.request, 'admin', defaultPassword); @@ -121,7 +116,8 @@ test('register via new-user invitation email', async ({ page }) => { const uuid = randomUUID(); - const newEmail = `${uuid}@mailinator.com`; + const mailbox = await mailboxFactory(); + const newEmail = mailbox.email; const addMemberModal = await projectPage.clickAddMember(); await addMemberModal.emailField.fill(newEmail); @@ -131,13 +127,12 @@ test('register via new-user invitation email', async ({ page }) => { await page.locator(':text("has been sent an invitation email")').waitFor(); // Check invite link returnTo is relative path, not absolute - const inboxPage = await getInbox(page, uuid).goto(); - const emailPage = await inboxPage.openEmail(EmailSubjects.ProjectInvitation); + const emailPage = await mailbox.openEmail(page, EmailSubjects.ProjectInvitation); const invitationUrl = await emailPage.getFirstLanguageDepotUrl(); expect(invitationUrl).not.toBeNull(); - expect(invitationUrl!).toContain('acceptInvitation'); - expect(invitationUrl!).toContain('returnTo='); - expect(invitationUrl!).not.toContain('returnTo=http'); + expect(invitationUrl).toContain('acceptInvitation'); + expect(invitationUrl).toContain('returnTo='); + expect(invitationUrl).not.toContain('returnTo=http'); // Click invite link, verify register page contains pre-filled email address const pagePromise = emailPage.page.context().waitForEvent('page'); diff --git a/frontend/tests/errorHandling.test.ts b/frontend/tests/errorHandling.test.ts index 6b810ea67..c7d6c0385 100644 --- a/frontend/tests/errorHandling.test.ts +++ b/frontend/tests/errorHandling.test.ts @@ -1,15 +1,14 @@ import * as testEnv from './envVars'; -import { AdminDashboardPage } from './pages/adminDashboardPage'; -import { EmailSubjects } from './pages/mailPages'; -import { LoginPage } from './pages/loginPage'; -import { SandboxPage } from './pages/sandboxPage'; -import { UserAccountSettingsPage } from './pages/userAccountSettingsPage'; -import { UserDashboardPage } from './pages/userDashboardPage'; -import { expect } from '@playwright/test'; -import { getInbox } from './utils/mailboxHelpers'; -import { loginAs } from './utils/authHelpers'; -import { test } from './fixtures'; +import {AdminDashboardPage} from './pages/adminDashboardPage'; +import {EmailSubjects} from './email/email-page'; +import {LoginPage} from './pages/loginPage'; +import {SandboxPage} from './pages/sandboxPage'; +import {UserAccountSettingsPage} from './pages/userAccountSettingsPage'; +import {UserDashboardPage} from './pages/userDashboardPage'; +import {expect} from '@playwright/test'; +import {loginAs} from './utils/authHelpers'; +import {test} from './fixtures'; test('can catch 500 errors from goto in same tab', async ({ page }) => { await new SandboxPage(page).goto(); @@ -151,8 +150,7 @@ test('page load 403 on home page is redirected to login', async ({ page, tempUse await page.locator(':text("Check Your Inbox")').first().waitFor(); // - Get JWT from reset password link - const inboxPage = await getInbox(page, tempUser.mailinatorId).goto(); - const emailPage = await inboxPage.openEmail(EmailSubjects.ForgotPassword); + const emailPage = await tempUser.mailbox.openEmail(page, EmailSubjects.ForgotPassword); const url = await emailPage.getFirstLanguageDepotUrl(); expect(url).not.toBeNull(); const forgotPasswordJwt = (url as string).split('jwt=')[1].split('&')[0]; diff --git a/frontend/tests/fixtures.ts b/frontend/tests/fixtures.ts index 280f341e5..a1266df15 100644 --- a/frontend/tests/fixtures.ts +++ b/frontend/tests/fixtures.ts @@ -1,18 +1,23 @@ -import { test as base, expect, type BrowserContext, type BrowserContextOptions } from '@playwright/test'; +import {test as base, expect, type BrowserContext, type BrowserContextOptions} from '@playwright/test'; import * as testEnv from './envVars'; -import { type UUID, randomUUID } from 'crypto'; -import { deleteUser, loginAs, registerUser } from './utils/authHelpers'; -import { executeGql, type GqlResult } from './utils/gqlHelpers'; -import { mkdtemp, rm } from 'fs/promises'; -import { join } from 'path'; -import { tmpdir } from 'os'; +import {type UUID, randomUUID} from 'crypto'; +import {deleteUser, loginAs, registerUser} from './utils/authHelpers'; +import {executeGql, type GqlResult} from './utils/gqlHelpers'; +import {mkdtemp, rm} from 'fs/promises'; +import {join} from 'path'; +import {tmpdir} from 'os'; +import type {Mailbox} from './email/mailbox'; +import {isDev} from './envVars'; +import {E2EMailboxApi} from './email/e2e-mailbox-module-patched'; +import {MaildevMailbox} from './email/maildev-mailbox'; +import {E2EMailbox} from './email/e2e-mailbox'; export interface TempUser { id: UUID - mailinatorId: UUID name: string email: string password: string + mailbox: Mailbox } export interface TempProject { @@ -26,9 +31,21 @@ export type CreateProjectResponse = {data: {createProject: {createProjectRespons type Fixtures = { contextFactory: (options: BrowserContextOptions) => Promise, uniqueTestId: string, - tempUser: TempUser, + tempUser: Readonly, tempProject: TempProject, tempDir: string, + mailboxFactory: () => Promise, +} + +async function getNewMailbox(context: BrowserContext): Promise { + if (isDev) { + const email = `${randomUUID()}@maildev.com`; + return new MaildevMailbox(email, context.request); + } else { + const mailbox = new E2EMailboxApi(); + const email = await mailbox.createEmailAddress(); + return new E2EMailbox(email, mailbox); + } } function addUnexpectedResponseListener(context: BrowserContext): void { @@ -73,19 +90,22 @@ export const test = base.extend({ const testId = `${testInfo.testId}-${shortId}`; await use(testId); }, - tempUser: async ({ browser, page }, use, testInfo) => { - const mailinatorId = randomUUID(); - const email = `${mailinatorId}@mailinator.com`; + mailboxFactory: async ({context}, use) => { + await use(() => getNewMailbox(context)); + }, + tempUser: async ({browser, page, mailboxFactory}, use, testInfo) => { + const mailbox = await mailboxFactory(); + const email = mailbox.email; const name = `Test: ${testInfo.title} - ${email}`; const password = email; const tempUserId = await registerUser(page, name, email, password); - const tempUser: TempUser = { + const tempUser = Object.freeze({ id: tempUserId, - mailinatorId, name, email, - password - }; + password, + mailbox, + }); await use(tempUser); const context = await browser.newContext(); await loginAs(context.request, 'admin', testEnv.defaultPassword); diff --git a/frontend/tests/pages/mailDevPages.ts b/frontend/tests/pages/mailDevPages.ts deleted file mode 100644 index a27edda7a..000000000 --- a/frontend/tests/pages/mailDevPages.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Page } from '@playwright/test'; -import { MailEmailPage, MailInboxPage } from './mailPages'; - -export class MailDevInboxPage extends MailInboxPage { - constructor(page: Page, mailboxId: string) { - super(page, - page.locator('ul.email-list'), - 'http://localhost:1080/#/', - mailboxId, - page.locator('ul.email-list li a')); - } - - override getEmailPage(): MailEmailPage { - return new MailDevEmailPage(this.page); - } - - override async refreshEmails(): Promise { - await this.page.getByTitle('Refresh emails').click(); - } - - override async goto(options?: {expectRedirect: boolean}): Promise { - await super.goto(options); - await this.page.locator('input.search-input').fill(this.mailboxId); - return this; - } -} - -export class MailDevEmailPage extends MailEmailPage { - constructor(page: Page) { - super(page, page.frameLocator('.preview-iframe:visible').locator('body'), undefined); - } -} diff --git a/frontend/tests/pages/mailPages.ts b/frontend/tests/pages/mailPages.ts deleted file mode 100644 index da61e89a4..000000000 --- a/frontend/tests/pages/mailPages.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { type Locator, type Page, expect } from '@playwright/test'; -import { BasePage } from './basePage'; -import { serverBaseUrl } from '../envVars'; - -export enum EmailSubjects { - VerifyEmail = 'Verify your e-mail address', - ForgotPassword = 'Forgot your password?', - PasswordChanged = 'Your password was changed', - ProjectInvitation = 'Project invitation:', -} - -export abstract class MailInboxPage extends BasePage { - public mailboxId: string; - readonly emailLocator: Locator; - - constructor(page: Page, testLocator: Locator, url: string, mailboxId: string, emailLocator: Locator) { - super(page, testLocator, url); - this.mailboxId = mailboxId; - this.emailLocator = emailLocator; - } - - abstract getEmailPage(): MailEmailPage; - abstract refreshEmails(): Promise; - - async gotoMailbox(mailboxId: string): Promise { - this.mailboxId = mailboxId; - return await this.goto(); - } - - async openEmail(subject: EmailSubjects, index = 0): Promise { - const email = this.emailLocator.locator(`:text("${subject}")`).nth(index); - // Emails may not be immediately available, so if they aren't, refresh the email list until they show up - await expect(async () => { - if (!await email.isVisible()) { - await this.refreshEmails(); - } - await email.click(); - }, `Failed to find email: ${subject} (${index})`).toPass({timeout: 10_000}); // This auto-retries on a reasonable schedule - return await this.getEmailPage().waitFor(); - } -} - -export abstract class MailEmailPage extends BasePage { - readonly bodyLocator: Locator; - public get resetPasswordButton(): Locator { return this.bodyLocator.getByRole('link', {name: 'Reset password'}); } - - constructor(page: Page, bodyLocator: Locator, url?: string) { - super(page, bodyLocator, url); - this.bodyLocator = bodyLocator; - } - - clickVerifyEmail(): Promise { - return this.bodyLocator.getByRole('link', {name: 'Verify e-mail'}).click(); - } - - clickResetPassword(): Promise { - return this.resetPasswordButton.click(); - } - - getFirstLanguageDepotUrl(): Promise { - return this.bodyLocator.locator(`a[href*='${serverBaseUrl}']`).first().getAttribute('href'); - } - - clickFirstLanguageDepotUrl(): Promise { - return this.bodyLocator.locator(`a[href*='${serverBaseUrl}']`).first().click(); - } -} diff --git a/frontend/tests/pages/mailinatorPages.ts b/frontend/tests/pages/mailinatorPages.ts deleted file mode 100644 index 78e66ad88..000000000 --- a/frontend/tests/pages/mailinatorPages.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Page } from '@playwright/test'; -import { MailEmailPage, MailInboxPage } from './mailPages'; - -export class MailinatorInboxPage extends MailInboxPage { - constructor(page: Page, mailboxId: string) { - super(page, - page.locator(':text("Public Messages")'), - `https://www.mailinator.com/v4/public/inboxes.jsp?to=${mailboxId}`, - mailboxId, - page.locator(`[id^='row_']`)); - } - - override getEmailPage(): MailEmailPage { - return new MailinatorEmailPage(this.page); - } - - override async refreshEmails(): Promise { - return Promise.resolve(); // Mailinator auto-refreshes - } - - override async goto(options?: {expectRedirect: boolean}): Promise { - this.url = `https://www.mailinator.com/v4/public/inboxes.jsp?to=${this.mailboxId}`; - return super.goto(options); - } -} - -export class MailinatorEmailPage extends MailEmailPage { - constructor(page: Page) { - super(page, page.frameLocator('#html_msg_body').locator('body'), undefined); - } - - override getFirstLanguageDepotUrl(): Promise { - // Mailinator sometimes swaps links out with its own that and redirect to the original, - // but the originals are made available in the links tab, which is always in the DOM - return this.page.locator('#email_pane').locator(`a[href*='jwt=']`).first().getAttribute('href'); - } -} diff --git a/frontend/tests/userPage.test.ts b/frontend/tests/userPage.test.ts index d8fe2f5ba..f6a0c771a 100644 --- a/frontend/tests/userPage.test.ts +++ b/frontend/tests/userPage.test.ts @@ -1,12 +1,11 @@ -import { loginAs, logout } from './utils/authHelpers'; +import {loginAs, logout} from './utils/authHelpers'; -import { EmailSubjects } from './pages/mailPages'; -import { UserAccountSettingsPage } from './pages/userAccountSettingsPage'; -import { UserDashboardPage } from './pages/userDashboardPage'; -import { expect } from '@playwright/test'; -import { getInbox } from './utils/mailboxHelpers'; -import { randomUUID } from 'crypto'; -import { test } from './fixtures'; +import {EmailSubjects} from './email/email-page'; +import {UserAccountSettingsPage} from './pages/userAccountSettingsPage'; +import {UserDashboardPage} from './pages/userDashboardPage'; +import {expect} from '@playwright/test'; +import {randomUUID} from 'crypto'; +import {test} from './fixtures'; test('can update account info', async ({ page, tempUser }) => { await loginAs(page.request, tempUser.email, tempUser.password); @@ -46,7 +45,6 @@ test('can reset password', async ({ page, tempUser }) => { await new UserDashboardPage(loginPage.page).waitFor(); // Verify password changed email was received - const inboxPage = await getInbox(page, tempUser.mailinatorId).goto(); - const emailPage = await inboxPage.openEmail(EmailSubjects.PasswordChanged); + const emailPage = await tempUser.mailbox.openEmail(page, EmailSubjects.PasswordChanged); await expect(emailPage.page.getByText('your password was just changed').first()).toBeVisible(); }); diff --git a/frontend/tests/utils/mailboxHelpers.ts b/frontend/tests/utils/mailboxHelpers.ts deleted file mode 100644 index aa74aacbf..000000000 --- a/frontend/tests/utils/mailboxHelpers.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Page } from '@playwright/test'; -import { isDev } from '../envVars'; -import type { MailInboxPage } from '../pages/mailPages'; -import { MailDevInboxPage } from '../pages/mailDevPages'; -import { MailinatorInboxPage } from '../pages/mailinatorPages'; - -// This used to be a static method of MailInboxPage in C#, -// but in Typescript that creates a circular import reference -export function getInbox(page: Page, mailboxId: string): MailInboxPage { - return isDev ? new MailDevInboxPage(page, mailboxId) : new MailinatorInboxPage(page, mailboxId); -}