Merge pull request #576 from QAComet/qacomet/login-tests
Added playwright test setup and login tests
This commit is contained in:
commit
eb8eb74a32
|
@ -0,0 +1,143 @@
|
|||
name: Linkwarden Playwright Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- qacomet/**
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
PGHOST: localhost
|
||||
PGPORT: 5432
|
||||
PGUSER: postgres
|
||||
PGPASSWORD: password
|
||||
PGDATABASE: postgres
|
||||
|
||||
TEST_POSTGRES_USER: test_linkwarden_user
|
||||
TEST_POSTGRES_PASSWORD: password
|
||||
TEST_POSTGRES_DATABASE: test_linkwarden_db
|
||||
TEST_POSTGRES_DATABASE_TEMPLATE: test_linkwarden_db_template
|
||||
TEST_POSTGRES_HOST: localhost
|
||||
TEST_POSTGREST_PORT: 5432
|
||||
PRODUCTION_POSTGRES_DATABASE: linkwarden_db
|
||||
|
||||
NEXTAUTH_SECRET: very_sensitive_secret
|
||||
NEXTAUTH_URL: http://localhost:3000/api/v1/auth
|
||||
|
||||
# Manual installation database settings
|
||||
DATABASE_URL: postgresql://test_linkwarden_user:password@localhost:5432/test_linkwarden_db
|
||||
|
||||
# Docker installation database settings
|
||||
POSTGRES_PASSWORD: password
|
||||
|
||||
TEST_USERNAME: test-user
|
||||
TEST_PASSWORD: password
|
||||
|
||||
jobs:
|
||||
playwright-test-runner:
|
||||
strategy:
|
||||
matrix:
|
||||
test_case: ['@login']
|
||||
timeout-minutes: 20
|
||||
runs-on:
|
||||
- ubuntu-22.04
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Initialize PostgreSQL
|
||||
run: |
|
||||
echo "Initializing Databases"
|
||||
psql -h localhost -U postgres -d postgres -c "CREATE USER ${{ env.TEST_POSTGRES_USER }} WITH PASSWORD '${{ env.TEST_POSTGRES_PASSWORD }}';"
|
||||
psql -h localhost -U postgres -d postgres -c "CREATE DATABASE ${{ env.TEST_POSTGRES_DATABASE }} OWNER ${{ env.TEST_POSTGRES_USER }};"
|
||||
|
||||
- name: Install packages
|
||||
run: yarn install -y
|
||||
|
||||
- name: Cache playwright dependencies
|
||||
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: |
|
||||
ffmpeg fonts-freefont-ttf fonts-ipafont-gothic fonts-tlwg-loma-otf
|
||||
fonts-unifont fonts-wqy-zenhei gstreamer1.0-libav gstreamer1.0-plugins-bad
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good libaa1 libass9
|
||||
libasyncns0 libavc1394-0 libavcodec58 libavdevice58 libavfilter7
|
||||
libavformat58 libavutil56 libbluray2 libbs2b0 libcaca0 libcdio-cdda2
|
||||
libcdio-paranoia2 libcdio19 libcdparanoia0 libchromaprint1 libcodec2-1.0
|
||||
libdc1394-25 libdca0 libdecor-0-0 libdv4 libdvdnav4 libdvdread8 libegl-mesa0
|
||||
libegl1 libevdev2 libevent-2.1-7 libfaad2 libffi7 libflac8 libflite1
|
||||
libfluidsynth3 libfreeaptx0 libgles2 libgme0 libgsm1 libgssdp-1.2-0
|
||||
libgstreamer-gl1.0-0 libgstreamer-plugins-bad1.0-0
|
||||
libgstreamer-plugins-base1.0-0 libgstreamer-plugins-good1.0-0 libgupnp-1.2-1
|
||||
libgupnp-igd-1.0-4 libharfbuzz-icu0 libhyphen0 libiec61883-0
|
||||
libinstpatch-1.0-2 libjack-jackd2-0 libkate1 libldacbt-enc2 liblilv-0-0
|
||||
libltc11 libmanette-0.2-0 libmfx1 libmjpegutils-2.1-0 libmodplug1
|
||||
libmp3lame0 libmpcdec6 libmpeg2encpp-2.1-0 libmpg123-0 libmplex2-2.1-0
|
||||
libmysofa1 libnice10 libnotify4 libopenal-data libopenal1 libopengl0
|
||||
libopenh264-6 libopenmpt0 libopenni2-0 libopus0 liborc-0.4-0
|
||||
libpocketsphinx3 libpostproc55 libpulse0 libqrencode4 libraw1394-11
|
||||
librubberband2 libsamplerate0 libsbc1 libsdl2-2.0-0 libserd-0-0 libshine3
|
||||
libshout3 libsndfile1 libsndio7.0 libsord-0-0 libsoundtouch1 libsoup-3.0-0
|
||||
libsoup-3.0-common libsoxr0 libspandsp2 libspeex1 libsphinxbase3
|
||||
libsratom-0-0 libsrt1.4-gnutls libsrtp2-1 libssh-gcrypt-4 libswresample3
|
||||
libswscale5 libtag1v5 libtag1v5-vanilla libtheora0 libtwolame0 libudfread0
|
||||
libv4l-0 libv4lconvert0 libva-drm2 libva-x11-2 libva2 libvdpau1
|
||||
libvidstab1.1 libvisual-0.4-0 libvo-aacenc0 libvo-amrwbenc0 libvorbisenc2
|
||||
libvpx7 libwavpack1 libwebrtc-audio-processing1 libwildmidi2 libwoff1
|
||||
libx264-163 libxcb-shape0 libxv1 libxvidcore4 libzbar0 libzimg2
|
||||
libzvbi-common libzvbi0 libzxingcore1 ocl-icd-libopencl1 timgm6mb-soundfont
|
||||
xfonts-cyrillic xfonts-encodings xfonts-scalable xfonts-utils
|
||||
|
||||
- name: Cache playwright browsers
|
||||
id: cache-playwright
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Install playwright
|
||||
if: steps.cache-playwright.outputs.cache-hit != 'true'
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
- name: Setup project
|
||||
run: |
|
||||
yarn prisma generate
|
||||
yarn build
|
||||
yarn prisma migrate deploy
|
||||
|
||||
- name: Start linkwarden server and worker
|
||||
run: yarn start &
|
||||
|
||||
- name: Run Tests
|
||||
run: npx playwright test --grep ${{ matrix.test_case }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: test-results
|
||||
retention-days: 30
|
|
@ -42,9 +42,15 @@ prisma/dev.db
|
|||
# tests
|
||||
/tests
|
||||
/test-results/
|
||||
/blob-report/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
/playwright/.auth/
|
||||
|
||||
# docker
|
||||
pgdata
|
||||
certificates
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
node_modules
|
||||
.next
|
||||
public
|
||||
/public
|
||||
|
||||
*.lock
|
||||
*.log
|
||||
|
|
|
@ -4,6 +4,7 @@ type Props = {
|
|||
loading?: boolean;
|
||||
className?: string;
|
||||
type?: "button" | "submit" | "reset" | undefined;
|
||||
"data-testid"?: string;
|
||||
};
|
||||
|
||||
export default function AccentSubmitButton({
|
||||
|
@ -12,6 +13,7 @@ export default function AccentSubmitButton({
|
|||
loading,
|
||||
className,
|
||||
type,
|
||||
"data-testid": dataTestId,
|
||||
}: Props) {
|
||||
return (
|
||||
<button
|
||||
|
@ -19,6 +21,7 @@ export default function AccentSubmitButton({
|
|||
className={`border primary-btn-gradient select-none duration-200 bg-black border-[oklch(var(--p))] hover:border-[#0070b5] rounded-lg text-center px-4 py-2 text-white active:scale-95 tracking-wider w-fit flex justify-center items-center gap-2 ${
|
||||
className || ""
|
||||
}`}
|
||||
data-testid={dataTestId}
|
||||
onClick={() => {
|
||||
if (loading !== undefined && !loading && onClick) onClick();
|
||||
}}
|
||||
|
|
|
@ -32,8 +32,14 @@ export default function Modal({ toggleModal, className, children }: Props) {
|
|||
<Drawer.Overlay className="fixed inset-0 bg-black/40" />
|
||||
<ClickAwayHandler onClickOutside={() => setDrawerIsOpen(false)}>
|
||||
<Drawer.Content className="flex flex-col rounded-t-2xl h-[90%] mt-24 fixed bottom-0 left-0 right-0 z-30">
|
||||
<div className="p-4 pb-32 bg-base-100 rounded-t-2xl flex-1 border-neutral-content border-t overflow-y-auto">
|
||||
<div className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-neutral mb-5" />
|
||||
<div
|
||||
className="p-4 pb-32 bg-base-100 rounded-t-2xl flex-1 border-neutral-content border-t overflow-y-auto"
|
||||
data-testid="mobile-modal-container"
|
||||
>
|
||||
<div
|
||||
className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-neutral mb-5"
|
||||
data-testid="mobile-modal-slider"
|
||||
/>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
@ -44,19 +50,28 @@ export default function Modal({ toggleModal, className, children }: Props) {
|
|||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="overflow-y-auto pt-2 sm:py-2 fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex justify-center items-center fade-in z-40">
|
||||
<div
|
||||
className="overflow-y-auto pt-2 sm:py-2 fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex justify-center items-center fade-in z-40"
|
||||
data-testid="modal-outer"
|
||||
>
|
||||
<ClickAwayHandler
|
||||
onClickOutside={toggleModal}
|
||||
className={`w-full mt-auto sm:m-auto sm:w-11/12 sm:max-w-2xl ${
|
||||
className || ""
|
||||
}`}
|
||||
>
|
||||
<div className="slide-up mt-auto sm:m-auto relative border-neutral-content rounded-t-2xl sm:rounded-2xl border-t sm:border shadow-2xl p-5 bg-base-100 overflow-y-auto sm:overflow-y-visible">
|
||||
<div
|
||||
className="slide-up mt-auto sm:m-auto relative border-neutral-content rounded-t-2xl sm:rounded-2xl border-t sm:border shadow-2xl p-5 bg-base-100 overflow-y-auto sm:overflow-y-visible"
|
||||
data-testid="modal-container"
|
||||
>
|
||||
<div
|
||||
onClick={toggleModal as MouseEventHandler<HTMLDivElement>}
|
||||
className="absolute top-4 right-3 btn btn-sm outline-none btn-circle btn-ghost z-10"
|
||||
>
|
||||
<i className="bi-x text-neutral text-2xl"></i>
|
||||
<i
|
||||
className="bi-x text-neutral text-2xl"
|
||||
data-testid="close-modal-button"
|
||||
></i>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,7 @@ type Props = {
|
|||
onKeyDown?: KeyboardEventHandler<HTMLInputElement> | undefined;
|
||||
className?: string;
|
||||
spellCheck?: boolean;
|
||||
"data-testid"?: string;
|
||||
};
|
||||
|
||||
export default function TextInput({
|
||||
|
@ -20,9 +21,11 @@ export default function TextInput({
|
|||
onKeyDown,
|
||||
className,
|
||||
spellCheck,
|
||||
"data-testid": dataTestId,
|
||||
}: Props) {
|
||||
return (
|
||||
<input
|
||||
data-testid={dataTestId}
|
||||
spellCheck={spellCheck}
|
||||
autoFocus={autoFocus}
|
||||
type={type ? type : "text"}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
TEST_USERNAME=test
|
||||
TEST_PASSWORD=password
|
|
@ -0,0 +1,20 @@
|
|||
import axios, { AxiosError } from "axios"
|
||||
|
||||
axios.defaults.baseURL = "http://localhost:3000"
|
||||
|
||||
export async function seedUser (username?: string, password?: string, name?: string) {
|
||||
try {
|
||||
return await axios.post("/api/v1/users", {
|
||||
username: username || "test",
|
||||
password: password || "password",
|
||||
name: name || "Test User",
|
||||
})
|
||||
} catch (e: any) {
|
||||
if (e instanceof AxiosError) {
|
||||
if (e.response?.status === 400) {
|
||||
return
|
||||
}
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { Locator, Page } from "playwright";
|
||||
import { BasePage } from "./page";
|
||||
|
||||
export class DashboardPage extends BasePage {
|
||||
container: Locator;
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.container = this.page.getByTestId("dashboard-wrapper");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { Locator, Page } from "@playwright/test";
|
||||
|
||||
export class BaseModal {
|
||||
page: Page;
|
||||
container: Locator;
|
||||
mobileContainer: Locator;
|
||||
closeModalButton: Locator;
|
||||
mobileModalSlider: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.container = page.getByTestId("modal-container");
|
||||
this.mobileContainer = page.getByTestId("mobile-modal-container");
|
||||
this.closeModalButton = this.container.getByTestId("close-modal-button");
|
||||
this.mobileModalSlider = this.mobileContainer.getByTestId(
|
||||
"mobile-modal-slider"
|
||||
);
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (await this.container.isVisible()) {
|
||||
await this.closeModalButton.click();
|
||||
}
|
||||
if (await this.mobileContainer.isVisible()) {
|
||||
const box = await this.mobileModalSlider.boundingBox();
|
||||
if (!box) {
|
||||
return;
|
||||
}
|
||||
const pageHeight = await this.page.evaluate(() => window.innerHeight);
|
||||
const startX = box.x + box.width / 2;
|
||||
const startY = box.y + box.height / 2;
|
||||
await this.page.mouse.move(startX, startY);
|
||||
await this.page.mouse.down();
|
||||
await this.page.mouse.move(startX, startY + pageHeight / 2);
|
||||
await this.page.mouse.up();
|
||||
}
|
||||
}
|
||||
|
||||
async isOpen() {
|
||||
return (
|
||||
(await this.container.isVisible()) ||
|
||||
(await this.mobileContainer.isVisible())
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { Locator, Page } from "@playwright/test";
|
||||
|
||||
export class BasePage {
|
||||
page: Page;
|
||||
toastMessage: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.toastMessage = this.page.getByTestId("toast-message-container");
|
||||
}
|
||||
|
||||
async getLatestToast() {
|
||||
const toast = this.toastMessage.first();
|
||||
return {
|
||||
locator: toast,
|
||||
closeButton: toast.getByTestId("close-toast-button"),
|
||||
message: toast.getByTestId("toast-message"),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { test as baseTest } from "@playwright/test";
|
||||
import { LoginPage } from "./login-page";
|
||||
import { RegistrationPage } from "./registration-page";
|
||||
import { DashboardPage } from "./base/dashboard-page";
|
||||
|
||||
export const test = baseTest.extend<{
|
||||
dashboardPage: DashboardPage;
|
||||
loginPage: LoginPage;
|
||||
registrationPage: RegistrationPage;
|
||||
}>({
|
||||
page: async ({ page }, use) => {
|
||||
await page.goto("/");
|
||||
use(page);
|
||||
},
|
||||
dashboardPage: async ({ page }, use) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await use(dashboardPage);
|
||||
},
|
||||
loginPage: async ({ page }, use) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await use(loginPage);
|
||||
},
|
||||
registrationPage: async ({ page }, use) => {
|
||||
const registrationPage = new RegistrationPage(page);
|
||||
await use(registrationPage);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
import { Locator, Page } from "@playwright/test";
|
||||
import { BasePage } from "./base/page";
|
||||
|
||||
export class LoginPage extends BasePage {
|
||||
submitLoginButton: Locator;
|
||||
loginForm: Locator;
|
||||
registerLink: Locator;
|
||||
passwordInput: Locator;
|
||||
usernameInput: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
|
||||
this.submitLoginButton = page.getByTestId("submit-login-button");
|
||||
|
||||
this.loginForm = page.getByTestId("login-form");
|
||||
|
||||
this.registerLink = page.getByTestId("register-link");
|
||||
|
||||
this.passwordInput = page.getByTestId("password-input");
|
||||
this.usernameInput = page.getByTestId("username-input");
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto("/login");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { Locator, Page } from "@playwright/test";
|
||||
import { BasePage } from "./base/page";
|
||||
|
||||
export class RegistrationPage extends BasePage {
|
||||
registerButton: Locator;
|
||||
registrationForm: Locator;
|
||||
|
||||
loginLink: Locator;
|
||||
|
||||
displayNameInput: Locator;
|
||||
passwordConfirmInput: Locator;
|
||||
passwordInput: Locator;
|
||||
usernameInput: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
|
||||
this.registerButton = page.getByTestId("register-button");
|
||||
this.registrationForm = page.getByTestId("registration-form");
|
||||
|
||||
this.loginLink = page.getByTestId("login-link");
|
||||
|
||||
this.displayNameInput = page.getByTestId("display-name-input");
|
||||
this.passwordConfirmInput = page.getByTestId("password-confirm-input");
|
||||
this.passwordInput = page.getByTestId("password-input");
|
||||
this.usernameInput = page.getByTestId("username-input");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export { test } from "./fixtures";
|
||||
export { expect } from "@playwright/test";
|
|
@ -0,0 +1,19 @@
|
|||
import { seedUser } from "@/e2e/data/user";
|
||||
import { test as setup } from "../../index";
|
||||
import { STORAGE_STATE } from "../../../playwright.config";
|
||||
|
||||
setup("Setup the default user", async ({ page, dashboardPage, loginPage }) => {
|
||||
const username = process.env["TEST_USERNAME"] || "";
|
||||
const password = process.env["TEST_PASSWORD"] || "";
|
||||
await seedUser(username, password);
|
||||
|
||||
await loginPage.goto();
|
||||
await loginPage.usernameInput.fill(username);
|
||||
await loginPage.passwordInput.fill(password);
|
||||
await loginPage.submitLoginButton.click();
|
||||
await dashboardPage.container.waitFor({ state: "visible" });
|
||||
|
||||
await page.context().storageState({
|
||||
path: STORAGE_STATE,
|
||||
});
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
import { seedUser } from "@/e2e/data/user";
|
||||
import { test as setup } from "../../index";
|
||||
|
||||
setup("Setup the default user", async () => {
|
||||
const username = process.env["TEST_USERNAME"] || "";
|
||||
const password = process.env["TEST_PASSWORD"] || "";
|
||||
await seedUser(username, password);
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
import { expect, test } from "../../index";
|
||||
|
||||
test.describe(
|
||||
"Login test suite",
|
||||
{
|
||||
tag: "@login",
|
||||
},
|
||||
async () => {
|
||||
test("Logging in without credentials displays an error", async ({
|
||||
loginPage,
|
||||
}) => {
|
||||
await loginPage.submitLoginButton.click();
|
||||
const toast = await loginPage.getLatestToast();
|
||||
await expect(toast.locator).toBeVisible();
|
||||
await expect(toast.locator).toHaveAttribute("data-type", "error");
|
||||
});
|
||||
|
||||
test("Logging in with an erroneous password displays an error", async ({
|
||||
loginPage,
|
||||
}) => {
|
||||
await loginPage.usernameInput.fill(process.env["TEST_USERNAME"] || "");
|
||||
await loginPage.passwordInput.fill("NOT_MY_PASSWORD_DNE_ERROR");
|
||||
await loginPage.submitLoginButton.click();
|
||||
const toast = await loginPage.getLatestToast();
|
||||
await expect(toast.locator).toBeVisible();
|
||||
await expect(toast.locator).toHaveAttribute("data-type", "error");
|
||||
});
|
||||
|
||||
test("Logging in without valid credentials displays an error", async ({
|
||||
loginPage,
|
||||
}) => {
|
||||
await loginPage.submitLoginButton.click();
|
||||
const toast = await loginPage.getLatestToast();
|
||||
await expect(toast.locator).toBeVisible();
|
||||
await expect(toast.locator).toHaveAttribute("data-type", "error");
|
||||
});
|
||||
|
||||
test("Logging in with a valid username and password works as expected", async ({
|
||||
page,
|
||||
loginPage,
|
||||
dashboardPage,
|
||||
}) => {
|
||||
await loginPage.usernameInput.fill(process.env["TEST_USERNAME"] || "");
|
||||
await loginPage.passwordInput.fill(process.env["TEST_PASSWORD"] || "");
|
||||
await loginPage.submitLoginButton.click();
|
||||
await expect(loginPage.loginForm).not.toBeVisible();
|
||||
await expect(dashboardPage.container).toBeVisible();
|
||||
});
|
||||
}
|
||||
);
|
|
@ -6,13 +6,21 @@ import React, { ReactNode, useEffect } from "react";
|
|||
interface Props {
|
||||
text?: string;
|
||||
children: ReactNode;
|
||||
"data-testid"?: string;
|
||||
}
|
||||
|
||||
export default function CenteredForm({ text, children }: Props) {
|
||||
export default function CenteredForm({
|
||||
text,
|
||||
children,
|
||||
"data-testid": dataTestId,
|
||||
}: Props) {
|
||||
const { settings } = useLocalSettingsStore();
|
||||
|
||||
return (
|
||||
<div className="absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center p-5">
|
||||
<div
|
||||
className="absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center p-5"
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
<div className="m-auto flex flex-col gap-2 w-full">
|
||||
{settings.theme ? (
|
||||
<Image
|
||||
|
|
|
@ -38,7 +38,7 @@ export default function MainLayout({ children }: Props) {
|
|||
<AnnouncementBar toggleAnnouncementBar={toggleAnnouncementBar} />
|
||||
) : undefined}
|
||||
|
||||
<div className="flex">
|
||||
<div className="flex" data-testid="dashboard-wrapper">
|
||||
<div className="hidden lg:block">
|
||||
<Sidebar
|
||||
className={`fixed ${showAnnouncement ? "top-10" : "top-0"}`}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
"start": "concurrently -P \"next start {@}\" \"yarn worker:prod\" --",
|
||||
"build": "next build",
|
||||
"lint": "next lint",
|
||||
"e2e": "playwright test e2e",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,js,json,md}\""
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -54,7 +55,7 @@
|
|||
"next-auth": "^4.22.1",
|
||||
"node-fetch": "^2.7.0",
|
||||
"nodemailer": "^6.9.3",
|
||||
"playwright": "^1.35.1",
|
||||
"playwright": "^1.43.1",
|
||||
"react": "18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "18.2.0",
|
||||
|
@ -69,7 +70,7 @@
|
|||
"zustand": "^4.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1",
|
||||
"@playwright/test": "^1.43.1",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/dompurify": "^3.0.4",
|
||||
"@types/jsdom": "^21.1.3",
|
||||
|
|
|
@ -69,7 +69,7 @@ export default function Login({
|
|||
function displayLoginCredential() {
|
||||
if (availableLogins.credentialsEnabled === "true") {
|
||||
return (
|
||||
<>
|
||||
<div data-testid="login-form">
|
||||
<p className="text-3xl text-black dark:text-white text-center font-extralight">
|
||||
Enter your credentials
|
||||
</p>
|
||||
|
@ -87,6 +87,7 @@ export default function Login({
|
|||
placeholder="johnny"
|
||||
value={form.username}
|
||||
className="bg-base-100"
|
||||
data-testid="username-input"
|
||||
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
@ -100,6 +101,7 @@ export default function Login({
|
|||
placeholder="••••••••••••••"
|
||||
value={form.password}
|
||||
className="bg-base-100"
|
||||
data-testid="password-input"
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
/>
|
||||
{availableLogins.emailEnabled === "true" && (
|
||||
|
@ -107,6 +109,7 @@ export default function Login({
|
|||
<Link
|
||||
href={"/forgot"}
|
||||
className="text-gray-500 dark:text-gray-400 font-semibold"
|
||||
data-testid="forgot-password-link"
|
||||
>
|
||||
Forgot Password?
|
||||
</Link>
|
||||
|
@ -117,13 +120,14 @@ export default function Login({
|
|||
type="submit"
|
||||
label="Login"
|
||||
className=" w-full text-center"
|
||||
data-testid="submit-login-button"
|
||||
loading={submitLoader}
|
||||
/>
|
||||
|
||||
{availableLogins.buttonAuths.length > 0 ? (
|
||||
<div className="divider my-1">OR</div>
|
||||
) : undefined}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -155,6 +159,7 @@ export default function Login({
|
|||
<Link
|
||||
href={"/register"}
|
||||
className="block text-black dark:text-white font-semibold"
|
||||
data-testid="register-link"
|
||||
>
|
||||
Sign Up
|
||||
</Link>
|
||||
|
|
|
@ -102,6 +102,7 @@ export default function Register() {
|
|||
} days of Premium Service at no cost!`
|
||||
: "Create a new account"
|
||||
}
|
||||
data-testid="registration-form"
|
||||
>
|
||||
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true" ? (
|
||||
<div className="p-4 flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
|
||||
|
@ -127,6 +128,7 @@ export default function Register() {
|
|||
placeholder="Johnny"
|
||||
value={form.name}
|
||||
className="bg-base-100"
|
||||
data-testid="display-name-input"
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
@ -139,6 +141,7 @@ export default function Register() {
|
|||
placeholder="john"
|
||||
value={form.username}
|
||||
className="bg-base-100"
|
||||
data-testid="username-input"
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, username: e.target.value })
|
||||
}
|
||||
|
@ -155,6 +158,7 @@ export default function Register() {
|
|||
placeholder="johnny@example.com"
|
||||
value={form.email}
|
||||
className="bg-base-100"
|
||||
data-testid="email-input"
|
||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
@ -168,6 +172,7 @@ export default function Register() {
|
|||
placeholder="••••••••••••••"
|
||||
value={form.password}
|
||||
className="bg-base-100"
|
||||
data-testid="password-input"
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
@ -182,6 +187,7 @@ export default function Register() {
|
|||
placeholder="••••••••••••••"
|
||||
value={form.passwordConfirmation}
|
||||
className="bg-base-100"
|
||||
data-testid="password-confirm-input"
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, passwordConfirmation: e.target.value })
|
||||
}
|
||||
|
@ -195,6 +201,7 @@ export default function Register() {
|
|||
<Link
|
||||
href="https://linkwarden.app/tos"
|
||||
className="font-semibold underline"
|
||||
data-testid="terms-of-service-link"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>{" "}
|
||||
|
@ -202,6 +209,7 @@ export default function Register() {
|
|||
<Link
|
||||
href="https://linkwarden.app/privacy-policy"
|
||||
className="font-semibold underline"
|
||||
data-testid="privacy-policy-link"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
|
@ -212,6 +220,7 @@ export default function Register() {
|
|||
<Link
|
||||
href="mailto:support@linkwarden.app"
|
||||
className="font-semibold underline"
|
||||
data-testid="support-link"
|
||||
>
|
||||
Get in touch
|
||||
</Link>
|
||||
|
@ -225,10 +234,15 @@ export default function Register() {
|
|||
label="Sign Up"
|
||||
className="w-full"
|
||||
loading={submitLoader}
|
||||
data-testid="register-button"
|
||||
/>
|
||||
<div className="flex items-baseline gap-1 justify-center">
|
||||
<p className="w-fit text-neutral">Already have an account?</p>
|
||||
<Link href={"/login"} className="block font-bold">
|
||||
<Link
|
||||
href={"/login"}
|
||||
className="block font-bold"
|
||||
data-testid="login-link"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import { defineConfig, devices } from "@playwright/test";
|
||||
import path from "path";
|
||||
import "dotenv/config.js";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
export const STORAGE_STATE = path.join(__dirname, "playwright/.auth/user.json");
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
|
@ -24,7 +21,7 @@ export default defineConfig({
|
|||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
baseURL: "http://127.0.0.1:3000",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
|
@ -33,10 +30,27 @@ export default defineConfig({
|
|||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
name: "setup dashboard",
|
||||
testMatch: /global\/setup\.dashboard\.ts/,
|
||||
},
|
||||
{
|
||||
name: "setup public",
|
||||
testMatch: /global\/setup\.public\.ts/,
|
||||
},
|
||||
{
|
||||
name: "chromium dashboard",
|
||||
dependencies: ["setup dashboard"],
|
||||
testMatch: "dashboard/*.spec.ts",
|
||||
use: { ...devices["Desktop Chrome"], storageState: STORAGE_STATE },
|
||||
},
|
||||
{
|
||||
name: "chromium public",
|
||||
dependencies: ["setup public"],
|
||||
testMatch: "public/*.spec.ts",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
/*
|
||||
{
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
|
@ -46,6 +60,7 @@ export default defineConfig({
|
|||
name: "webkit",
|
||||
use: { ...devices["Desktop Safari"] },
|
||||
},
|
||||
*/
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
|
|
33
yarn.lock
33
yarn.lock
|
@ -1279,15 +1279,12 @@
|
|||
tiny-glob "^0.2.9"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@playwright/test@^1.35.1":
|
||||
version "1.35.1"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.35.1.tgz#a596b61e15b980716696f149cc7a2002f003580c"
|
||||
integrity sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==
|
||||
"@playwright/test@^1.43.1":
|
||||
version "1.43.1"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.43.1.tgz#16728a59eb8ce0f60472f98d8886d6cab0fa3e42"
|
||||
integrity sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
playwright-core "1.35.1"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
playwright "1.43.1"
|
||||
|
||||
"@prisma/client@^4.16.2":
|
||||
version "4.16.2"
|
||||
|
@ -4920,17 +4917,19 @@ pixelmatch@^4.0.2:
|
|||
dependencies:
|
||||
pngjs "^3.0.0"
|
||||
|
||||
playwright-core@1.35.1:
|
||||
version "1.35.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.35.1.tgz#52c1e6ffaa6a8c29de1a5bdf8cce0ce290ffb81d"
|
||||
integrity sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==
|
||||
playwright-core@1.43.1:
|
||||
version "1.43.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.43.1.tgz#0eafef9994c69c02a1a3825a4343e56c99c03b02"
|
||||
integrity sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==
|
||||
|
||||
playwright@^1.35.1:
|
||||
version "1.35.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.35.1.tgz#f991d0c76ae517d4a0023d9428b09d19d5e87128"
|
||||
integrity sha512-NbwBeGJLu5m7VGM0+xtlmLAH9VUfWwYOhUi/lSEDyGg46r1CA9RWlvoc5yywxR9AzQb0mOCm7bWtOXV7/w43ZA==
|
||||
playwright@1.43.1, playwright@^1.43.1:
|
||||
version "1.43.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.43.1.tgz#8ad08984ac66c9ef3d0db035be54dd7ec9f1c7d9"
|
||||
integrity sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==
|
||||
dependencies:
|
||||
playwright-core "1.35.1"
|
||||
playwright-core "1.43.1"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
pngjs@^3.0.0, pngjs@^3.3.3:
|
||||
version "3.4.0"
|
||||
|
|
Ŝarĝante…
Reference in New Issue