Support updating problems

This commit is contained in:
Jason Laster 2025-03-19 15:03:58 -07:00
parent 0d3c3eb471
commit a5e53a023b
16 changed files with 284 additions and 67 deletions

View File

@ -50,6 +50,8 @@ jobs:
env:
SUPABASE_TEST_USER_EMAIL: ${{ secrets.SUPABASE_TEST_USER_EMAIL }}
SUPABASE_TEST_USER_PASSWORD: ${{ secrets.SUPABASE_TEST_USER_PASSWORD }}
NUT_LOGIN_KEY: ${{ secrets.NUT_LOGIN_KEY }}
NUT_PASSWORD: ${{ secrets.NUT_PASSWORD }}
- name: Upload test results
if: always()
@ -99,7 +101,9 @@ jobs:
PLAYWRIGHT_TEST_BASE_URL: ${{ steps.deploy.outputs.WITH_SUPABASE_PREVIEW_URL }}
SUPABASE_TEST_USER_EMAIL: ${{ secrets.SUPABASE_TEST_USER_EMAIL }}
SUPABASE_TEST_USER_PASSWORD: ${{ secrets.SUPABASE_TEST_USER_PASSWORD }}
NUT_LOGIN_KEY: ${{ secrets.NUT_LOGIN_KEY }}
NUT_PASSWORD: ${{ secrets.NUT_PASSWORD }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4

View File

@ -118,7 +118,7 @@ export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
</div>
</div>
<RadixDialog.Close asChild onClick={onClose}>
<IconButton icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
<IconButton testId="dialog-close" icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
</RadixDialog.Close>
</motion.div>
</RadixDialog.Content>

View File

@ -1,12 +1,13 @@
import { useState } from 'react';
import { useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import Cookies from 'js-cookie';
import { anthropicNumFreeUsesCookieName, anthropicApiKeyCookieName, maxFreeUses } from '~/utils/freeUses';
import { saveNutLoginKey, saveProblemsUsername, getNutLoginKey, getProblemsUsername } from '~/lib/replay/Problems';
import { saveNutLoginKey, saveUsername, getNutLoginKey, getUsername } from '~/lib/replay/Problems';
import { debounce } from '~/utils/debounce';
export default function ConnectionsTab() {
const [apiKey, setApiKey] = useState(Cookies.get(anthropicApiKeyCookieName) || '');
const [username, setUsername] = useState(getProblemsUsername() || '');
const [username, setUsername] = useState(getUsername() || '');
const [loginKey, setLoginKey] = useState(getNutLoginKey() || '');
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
@ -20,9 +21,16 @@ export default function ConnectionsTab() {
setApiKey(key);
};
const saveUsernameWithToast = (username: string) => {
saveUsername(username);
toast.success('Username saved!');
};
const debouncedSaveUsername = useCallback(debounce(saveUsernameWithToast, 1000), []);
const handleSaveUsername = async (username: string) => {
saveProblemsUsername(username);
setUsername(username);
debouncedSaveUsername(username);
};
const handleSaveLoginKey = async (key: string) => {
@ -61,6 +69,7 @@ export default function ConnectionsTab() {
<div className="flex-1 mr-2">
<input
type="text"
placeholder="Enter your username"
value={username}
onChange={(e) => handleSaveUsername(e.target.value)}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50"
@ -72,6 +81,7 @@ export default function ConnectionsTab() {
<div className="flex-1 mr-2">
<input
type="text"
placeholder="Enter your login key"
value={loginKey}
onChange={(e) => handleSaveLoginKey(e.target.value)}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50"

View File

@ -1,6 +1,7 @@
import { motion, type Variants } from 'framer-motion';
import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { useStore } from '@nanostores/react';
import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
import { SettingsWindow } from '~/components/settings/SettingsWindow';
@ -13,7 +14,7 @@ import { binDates } from './date-binning';
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
import { SaveProblem } from './SaveProblem';
import { SaveReproductionModal } from './SaveReproduction';
import { getNutIsAdmin } from '~/lib/replay/Problems';
import { isAdminStore } from '~/lib/stores/user';
const menuVariants = {
closed: {
@ -46,6 +47,7 @@ export const Menu = () => {
const [open, setOpen] = useState(false);
const [dialogContent, setDialogContent] = useState<DialogContent>(null);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const isAdmin = useStore(isAdminStore);
const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({
items: list,
@ -140,7 +142,7 @@ export const Menu = () => {
Problems
</a>
<SaveProblem />
{getNutIsAdmin() && <SaveReproductionModal />}
{isAdmin && <SaveReproductionModal />}
<a
href="/about"
className="flex gap-2 bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"

View File

@ -2,7 +2,7 @@ import { toast } from 'react-toastify';
import ReactModal from 'react-modal';
import { useState, useEffect } from 'react';
import { workbenchStore } from '~/lib/stores/workbench';
import { getProblemsUsername, submitProblem, saveProblemsUsername, BoltProblemStatus } from '~/lib/replay/Problems';
import { getUsername, submitProblem, saveUsername, BoltProblemStatus } from '~/lib/replay/Problems';
import type { BoltProblemInput } from '~/lib/replay/Problems';
import { getRepositoryContents } from '~/lib/replay/Repository';
import { shouldUseSupabase, getCurrentUser, isAuthenticated } from '~/lib/supabase/client';
@ -31,7 +31,7 @@ export function SaveProblem() {
} else {
setIsLoggedIn(true); // Always considered logged in when not using Supabase
const username = getProblemsUsername();
const username = getUsername();
if (username) {
setFormData((prev) => ({ ...prev, username }));
@ -70,7 +70,7 @@ export function SaveProblem() {
// Only save username to cookie if not using Supabase
if (!shouldUseSupabase()) {
saveProblemsUsername(formData.username);
saveUsername(formData.username);
}
toast.info('Submitting problem...');

View File

@ -124,7 +124,7 @@ export const Dialog = memo(({ className, children, onBackdrop, onClose }: Dialog
>
{children}
<RadixDialog.Close asChild onClick={onClose}>
<IconButton icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
<IconButton testId="dialog-close" icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
</RadixDialog.Close>
</motion.div>
</RadixDialog.Content>

View File

@ -17,11 +17,13 @@ interface BaseIconButtonProps {
type IconButtonWithoutChildrenProps = {
icon: string;
children?: undefined;
testId?: string;
} & BaseIconButtonProps;
type IconButtonWithChildrenProps = {
icon?: undefined;
children: string | JSX.Element | JSX.Element[];
testId?: string;
} & BaseIconButtonProps;
type IconButtonProps = IconButtonWithoutChildrenProps | IconButtonWithChildrenProps;
@ -37,6 +39,7 @@ export const IconButton = memo(
iconClassName,
disabledClassName,
disabled = false,
testId,
title,
onClick,
children,
@ -56,6 +59,7 @@ export const IconButton = memo(
)}
title={title}
disabled={disabled}
data-testid={testId || 'icon-button'}
onClick={(event) => {
if (disabled) {
return;

View File

@ -13,6 +13,8 @@ import {
supabaseSubmitFeedback,
supabaseDeleteProblem,
} from '~/lib/supabase/problems';
import { getNutIsAdmin as getNutIsAdminFromSupabase } from '~/lib/supabase/client';
import { updateIsAdmin, updateUsername } from '~/lib/stores/user';
export interface BoltProblemComment {
username?: string;
@ -59,26 +61,41 @@ export interface BoltProblem extends BoltProblemDescription {
export type BoltProblemInput = Omit<BoltProblem, 'problemId' | 'timestamp'>;
export async function listAllProblems(): Promise<BoltProblemDescription[]> {
let problems: BoltProblemDescription[] = [];
if (shouldUseSupabase()) {
return supabaseListAllProblems();
problems = await supabaseListAllProblems();
} else {
try {
const rv = await sendCommandDedicatedClient({
method: 'Recording.globalExperimentalCommand',
params: {
name: 'listBoltProblems',
},
});
console.log('ListProblemsRval', rv);
problems = (rv as any).rval.problems.reverse();
const filteredProblems = problems.filter((problem) => {
// if ?showAll=true is not in the url, filter out [test] problems
if (window.location.search.includes('showAll=true')) {
return true;
}
return !problem.title.includes('[test]');
});
return filteredProblems;
} catch (error) {
console.error('Error fetching problems', error);
toast.error('Failed to fetch problems');
return [];
}
}
try {
const rv = await sendCommandDedicatedClient({
method: 'Recording.globalExperimentalCommand',
params: {
name: 'listBoltProblems',
},
});
console.log('ListProblemsRval', rv);
return (rv as any).rval.problems.reverse();
} catch (error) {
console.error('Error fetching problems', error);
toast.error('Failed to fetch problems');
return [];
}
return problems;
}
export async function getProblem(problemId: string): Promise<BoltProblem | null> {
@ -192,13 +209,18 @@ export async function updateProblem(problemId: string, problem: BoltProblemInput
const nutLoginKeyCookieName = 'nutLoginKey';
const nutIsAdminCookieName = 'nutIsAdmin';
const nutUsernameCookieName = 'nutUsername';
export function getNutLoginKey(): string | undefined {
const cookieValue = Cookies.get(nutLoginKeyCookieName);
return cookieValue?.length ? cookieValue : undefined;
}
export function getNutIsAdmin(): boolean {
export async function getNutIsAdmin(): Promise<boolean> {
if (shouldUseSupabase()) {
return getNutIsAdminFromSupabase();
}
return Cookies.get(nutIsAdminCookieName) === 'true';
}
@ -222,22 +244,26 @@ export async function saveNutLoginKey(key: string) {
console.log('UserInfo', userInfo);
Cookies.set(nutLoginKeyCookieName, key);
Cookies.set(nutIsAdminCookieName, userInfo.admin ? 'true' : 'false');
setNutIsAdmin(userInfo.admin);
}
export function setNutIsAdmin(isAdmin: boolean) {
Cookies.set(nutIsAdminCookieName, isAdmin ? 'true' : 'false');
// Update the store
updateIsAdmin(isAdmin);
}
const nutProblemsUsernameCookieName = 'nutProblemsUsername';
export function getProblemsUsername(): string | undefined {
const cookieValue = Cookies.get(nutProblemsUsernameCookieName);
export function getUsername(): string | undefined {
const cookieValue = Cookies.get(nutUsernameCookieName);
return cookieValue?.length ? cookieValue : undefined;
}
export function saveProblemsUsername(username: string) {
Cookies.set(nutProblemsUsernameCookieName, username);
export function saveUsername(username: string) {
Cookies.set(nutUsernameCookieName, username);
// Update the store
updateUsername(username);
}
export async function submitFeedback(feedback: any): Promise<boolean> {

54
app/lib/stores/user.ts Normal file
View File

@ -0,0 +1,54 @@
import { atom } from 'nanostores';
import { getNutIsAdmin, getUsername } from '~/lib/replay/Problems';
import { userStore } from './auth';
// Store for admin status
export const isAdminStore = atom<boolean>(false);
// Store for username
export const usernameStore = atom<string | undefined>(undefined);
// Safe store updaters that check for browser environment
export function updateIsAdmin(value: boolean) {
if (typeof window !== 'undefined') {
isAdminStore.set(value);
}
}
export function updateUsername(username: string | undefined) {
if (typeof window !== 'undefined') {
usernameStore.set(username);
}
}
// Initialize the user stores
export async function initializeUserStores() {
try {
// Only run in browser environment
if (typeof window === 'undefined') {
return undefined;
}
// Initialize with current values
const isAdmin = await getNutIsAdmin();
isAdminStore.set(isAdmin);
const username = getUsername();
usernameStore.set(username);
// Subscribe to user changes to update admin status
return userStore.subscribe(async (user) => {
if (user) {
// When user changes, recalculate admin status
const isAdmin = await getNutIsAdmin();
isAdminStore.set(isAdmin);
} else {
// Reset when logged out
isAdminStore.set(false);
}
});
} catch (error) {
console.error('Failed to initialize user stores', error);
return undefined;
}
}

View File

@ -54,6 +54,9 @@ export interface Database {
let supabaseUrl = '';
let supabaseAnonKey = '';
// Add a singleton client instance
let supabaseClientInstance: ReturnType<typeof createClient<Database>> | null = null;
/**
* Determines whether Supabase should be used based on URL parameters and environment variables.
* URL parameters take precedence over environment variables.
@ -86,6 +89,27 @@ export async function getCurrentUser(): Promise<SupabaseUser | null> {
}
}
export async function getNutIsAdmin(): Promise<boolean> {
const user = await getCurrentUser();
if (!user) {
return false;
}
const { data: profileData, error: profileError } = await getSupabase()
.from('profiles')
.select('is_admin')
.eq('id', user?.id)
.single();
if (profileError) {
console.error('Error fetching user profile:', profileError);
return false;
}
return profileData?.is_admin || false;
}
/**
* Checks if there is a currently authenticated user.
*/
@ -95,6 +119,11 @@ export async function isAuthenticated(): Promise<boolean> {
}
export function getSupabase() {
// If we already have an instance, return it
if (supabaseClientInstance) {
return supabaseClientInstance;
}
// Determine execution environment and get appropriate variables
if (typeof window == 'object') {
supabaseUrl = window.ENV.SUPABASE_URL || '';
@ -115,13 +144,15 @@ export function getSupabase() {
console.warn('Missing Supabase environment variables. Some features may not work properly.');
}
// Create and return the Supabase client
return createClient<Database>(supabaseUrl, supabaseAnonKey, {
// Create and cache the Supabase client
supabaseClientInstance = createClient<Database>(supabaseUrl, supabaseAnonKey, {
auth: {
persistSession: true,
autoRefreshToken: true,
},
});
return supabaseClientInstance;
}
// Helper function to check if Supabase is properly initialized

View File

@ -3,7 +3,7 @@
import { toast } from 'react-toastify';
import { getSupabase, type Database } from './client';
import type { BoltProblem, BoltProblemDescription, BoltProblemInput, BoltProblemStatus } from '~/lib/replay/Problems';
import { getProblemsUsername, getNutIsAdmin } from '~/lib/replay/Problems';
import { getUsername, getNutIsAdmin } from '~/lib/replay/Problems';
export async function supabaseListAllProblems(): Promise<BoltProblemDescription[]> {
try {
@ -12,8 +12,6 @@ export async function supabaseListAllProblems(): Promise<BoltProblemDescription[
.select('id, created_at, updated_at, title, description, status, keywords, user_id')
.order('created_at', { ascending: false });
console.log('ListAllProblemsData', data);
if (error) {
throw error;
}
@ -197,7 +195,7 @@ export async function supabaseUpdateProblem(problemId: string, problem: BoltProb
const commentInserts = comments.map((comment) => ({
problem_id: problemId,
content: comment.content,
username: comment.username || getProblemsUsername() || 'Anonymous',
username: comment.username || getUsername() || 'Anonymous',
/**
* Use timestamp as a unique identifier for the comment.

View File

@ -10,6 +10,7 @@ import { createHead } from 'remix-island';
import { useEffect, useState } from 'react';
import { logStore } from './lib/stores/logs';
import { initializeAuth, userStore, isLoadingStore } from './lib/stores/auth';
import { initializeUserStores } from './lib/stores/user';
import { ToastContainer } from 'react-toastify';
import { Analytics } from '@vercel/analytics/remix';
import { AuthModal } from './components/auth/AuthModal';
@ -115,9 +116,14 @@ function AuthProvider({ data }: { data: LoaderData }) {
useEffect(() => {
if (typeof window !== 'undefined') {
window.ENV = data.ENV;
// Initialize auth and user stores
initializeAuth().catch((err: Error) => {
logStore.logError('Failed to initialize auth', err);
});
initializeUserStores().catch((err: Error) => {
logStore.logError('Failed to initialize user stores', err);
});
}
}, [data]);

View File

@ -7,14 +7,14 @@ import { ToastContainerWrapper, Status, Keywords } from './problems';
import { toast } from 'react-toastify';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { useParams } from '@remix-run/react';
import { useStore } from '@nanostores/react';
import {
getProblem,
updateProblem as backendUpdateProblem,
deleteProblem as backendDeleteProblem,
getProblemsUsername,
BoltProblemStatus,
getNutIsAdmin,
} from '~/lib/replay/Problems';
import { isAdminStore, usernameStore } from '~/lib/stores/user';
import type { BoltProblem, BoltProblemComment } from '~/lib/replay/Problems';
function Comments({ comments }: { comments: BoltProblemComment[] }) {
@ -57,10 +57,12 @@ interface UpdateProblemFormProps {
handleSubmit: (content: string) => void;
updateText: string;
placeholder: string;
inputType?: 'textarea' | 'select';
options?: { value: string; label: string }[];
}
function UpdateProblemForm(props: UpdateProblemFormProps) {
const { handleSubmit, updateText, placeholder } = props;
const { handleSubmit, updateText, placeholder, inputType = 'textarea', options = [] } = props;
const [value, setValue] = useState('');
const onSubmitClicked = (e: React.FormEvent<HTMLFormElement>) => {
@ -74,14 +76,32 @@ function UpdateProblemForm(props: UpdateProblemFormProps) {
return (
<form onSubmit={onSubmitClicked} className="mb-6 p-4 bg-bolt-elements-background-depth-2 rounded-lg">
<textarea
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={placeholder}
rows={4}
className="w-full p-3 mb-3 bg-bolt-elements-background-depth-3 rounded-md border border-bolt-elements-background-depth-4 text-black placeholder-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y min-h-[100px]"
required
/>
{inputType === 'textarea' ? (
<textarea
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={placeholder}
rows={4}
className="w-full p-3 mb-3 bg-bolt-elements-background-depth-3 rounded-md border border-bolt-elements-background-depth-4 text-black placeholder-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y min-h-[100px]"
required
/>
) : (
<select
value={value}
onChange={(e) => setValue(e.target.value)}
className="w-full p-3 mb-3 bg-bolt-elements-background-depth-3 rounded-md border border-bolt-elements-background-depth-4 text-black focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
>
<option value="" disabled>
{placeholder}
</option>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
)}
<button
type="submit"
disabled={!value.trim()}
@ -104,10 +124,12 @@ function UpdateProblemForms({
updateProblem: UpdateProblemCallback;
deleteProblem: DeleteProblemCallback;
}) {
const username = useStore(usernameStore);
const handleAddComment = (content: string) => {
const newComment: BoltProblemComment = {
timestamp: Date.now(),
username: getProblemsUsername(),
username,
content,
};
updateProblem((problem) => {
@ -158,6 +180,12 @@ function UpdateProblemForms({
}));
};
// Convert BoltProblemStatus enum to options array for select
const statusOptions = Object.entries(BoltProblemStatus).map(([key, _value]) => ({
value: key,
label: key,
}));
return (
<>
<UpdateProblemForm handleSubmit={handleAddComment} updateText="Add Comment" placeholder="Add a comment..." />
@ -174,7 +202,9 @@ function UpdateProblemForms({
<UpdateProblemForm
handleSubmit={handleSetStatus}
updateText="Set Status"
placeholder="Set the status of the problem..."
placeholder="Select a status..."
inputType="select"
options={statusOptions}
/>
<UpdateProblemForm
handleSubmit={handleSetKeywords}
@ -199,6 +229,7 @@ const Nothing = () => null;
function ViewProblemPage() {
const params = useParams();
const problemId = params.id;
const isAdmin = useStore(isAdminStore);
if (typeof problemId !== 'string') {
throw new Error('Problem ID is required');
@ -248,9 +279,7 @@ function ViewProblemPage() {
<ProblemViewer problem={problemData} />
)}
</div>
{getNutIsAdmin() && problemData && (
<UpdateProblemForms updateProblem={updateProblem} deleteProblem={deleteProblem} />
)}
{isAdmin && problemData && <UpdateProblemForms updateProblem={updateProblem} deleteProblem={deleteProblem} />}
<ToastContainerWrapper />
</div>
</TooltipProvider>

View File

@ -10,6 +10,8 @@
"deploy": "vercel deploy --prod",
"build": "npx remix vite:build",
"dev": "node pre-start.cjs && npx remix vite:dev",
"dev:supabase": "USE_SUPABASE=true node pre-start.cjs && npx remix vite:dev",
"dev:legacy": "USE_SUPABASE=false node pre-start.cjs && npx remix vite:dev",
"test": "vitest --run --exclude tests/e2e",
"test:watch": "vitest",
"test:e2e": "playwright test",

View File

@ -1,5 +1,5 @@
import { test, expect } from '@playwright/test';
import { isSupabaseEnabled } from './setup/test-utils';
import { isSupabaseEnabled, login, setLoginKey } from './setup/test-utils';
test('Should be able to load a problem', async ({ page }) => {
await page.goto('/problems');
@ -34,13 +34,7 @@ test('Should be able to save a problem ', async ({ page }) => {
await page.getByRole('button', { name: 'Log In' }).click();
await page.getByRole('textbox', { name: 'Email' }).click();
const email = process.env.SUPABASE_TEST_USER_EMAIL || '';
const password = process.env.SUPABASE_TEST_USER_PASSWORD || '';
await page.getByRole('textbox', { name: 'Email' }).fill(email);
await page.getByRole('textbox', { name: 'Email' }).press('Tab');
await page.getByRole('textbox', { name: 'Password' }).fill(password);
await page.getByRole('textbox', { name: 'Password' }).press('Enter');
await login(page);
await expect(page.getByText('Import the "problem" folder')).toBeVisible({ timeout: 30000 });
@ -68,3 +62,35 @@ test('Should be able to save a problem ', async ({ page }) => {
await page.getByRole('button', { name: 'Close' }).click();
}
});
test('Should be able to update a problem', async ({ page }) => {
await page.goto('/problems?showAll=true');
await page.getByRole('combobox').selectOption('all');
await page.getByRole('link', { name: '[test] playwright' }).first().click();
expect(await page.getByRole('textbox', { name: 'Set the title of the problem' })).not.toBeVisible();
if (await isSupabaseEnabled(page)) {
await login(page);
} else {
await setLoginKey(page);
}
const currentTime = new Date();
const hours = currentTime.getHours().toString().padStart(2, '0');
const minutes = currentTime.getMinutes().toString().padStart(2, '0');
const timeString = `${hours}:${minutes}`;
const title = `[test] playwright ${timeString}`;
await page.getByRole('textbox', { name: 'Set the title of the problem' }).click();
await page.getByRole('textbox', { name: 'Set the title of the problem' }).fill(title);
await page.getByRole('button', { name: 'Set Title' }).click();
await page.getByRole('heading', { name: title }).click();
await page.getByRole('combobox').selectOption('Solved');
await page.getByRole('button', { name: 'Set Status' }).click();
await page.locator('span').filter({ hasText: 'Solved' }).click();
await page.getByRole('combobox').selectOption('Pending');
await page.getByRole('button', { name: 'Set Status' }).click();
await page.locator('span').filter({ hasText: 'Pending' }).click();
});

View File

@ -53,3 +53,28 @@ export async function clickButton(page: Page, selector: string): Promise<void> {
export async function getElementText(page: Page, selector: string): Promise<string> {
return page.locator(selector).textContent() as Promise<string>;
}
export async function login(page: Page): Promise<void> {
await page.getByRole('button', { name: 'Sign In' }).click();
await page.getByRole('textbox', { name: 'Email' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill(process.env.SUPABASE_TEST_USER_EMAIL || '');
await page.getByRole('textbox', { name: 'Email' }).press('Tab');
await page.getByRole('textbox', { name: 'Password' }).fill(process.env.SUPABASE_TEST_USER_PASSWORD || '');
await page.getByRole('textbox', { name: 'Password' }).press('Enter');
}
export async function setLoginKey(page: Page): Promise<void> {
await page.locator('[data-testid="sidebar-icon"]').click();
await page.getByRole('button', { name: 'Settings' }).click();
await page.getByRole('button', { name: 'User Info' }).click();
await page.getByRole('textbox').nth(1).click();
await page.getByRole('textbox', { name: 'Enter your username' }).click();
await page.getByRole('textbox', { name: 'Enter your login key' }).click();
await page.getByRole('textbox', { name: 'Enter your login key' }).fill(process.env.NUT_LOGIN_KEY || '');
await page.getByRole('textbox', { name: 'Enter your username' }).click();
await page.getByRole('textbox', { name: 'Enter your username' }).fill(process.env.NUT_USERNAME || '');
await page.getByTestId('dialog-close').click();
}