diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index 5de4dd61..0ab55103 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -33,7 +33,7 @@ import { assert, generateRandomId } from '~/lib/replay/ReplayProtocolClient'; import { getMessagesRepositoryId, getPreviousRepositoryId, type Message } from '~/lib/persistence/message'; import { useAuthStatus } from '~/lib/stores/auth'; import { debounce } from '~/utils/debounce'; -import { supabaseSubmitFeedback } from '~/lib/supabase/problems'; +import { supabaseSubmitFeedback } from '~/lib/supabase/feedback'; const toastAnimation = cssTransition({ enter: 'animated fadeInRight', diff --git a/app/components/chat/LoadProblemButton.tsx b/app/components/chat/LoadProblemButton.tsx deleted file mode 100644 index c4e66775..00000000 --- a/app/components/chat/LoadProblemButton.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { useState } from 'react'; -import { toast } from 'react-toastify'; -import { logStore } from '~/lib/stores/logs'; -import { assert } from '~/lib/replay/ReplayProtocolClient'; -import type { NutProblem } from '~/lib/replay/Problems'; -import { getProblem } from '~/lib/replay/Problems'; -import { createMessagesForRepository, type Message } from '~/lib/persistence/message'; - -interface LoadProblemButtonProps { - className?: string; - importChat?: (description: string, messages: Message[]) => Promise; -} - -export function setLastLoadedProblem(problem: NutProblem) { - localStorage.setItem('loadedProblemId', problem.problemId); -} - -export async function getOrFetchLastLoadedProblem(): Promise { - let problem: NutProblem | null = null; - const problemId = localStorage.getItem('loadedProblemId'); - - if (!problemId) { - return null; - } - - problem = await getProblem(problemId); - - if (!problem) { - return null; - } - - return problem; -} - -export async function loadProblem( - problemId: string, - importChat: (description: string, messages: Message[]) => Promise, -) { - const problem = await getProblem(problemId); - - if (!problem) { - return; - } - - setLastLoadedProblem(problem); - - const { repositoryId, title: problemTitle } = problem; - - try { - const messages = createMessagesForRepository(`Problem: ${problemTitle}`, repositoryId); - await importChat(`Problem: ${problemTitle}`, messages); - - logStore.logSystem('Problem loaded successfully', { - problemId, - }); - toast.success('Problem loaded successfully'); - } catch (error) { - logStore.logError('Failed to load problem', error); - console.error('Failed to load problem:', error); - toast.error('Failed to load problem'); - } -} - -export const LoadProblemButton: React.FC = ({ className, importChat }) => { - const [isLoading, setIsLoading] = useState(false); - const [isInputOpen, setIsInputOpen] = useState(false); - - const handleSubmit = async (_e: React.ChangeEvent) => { - setIsLoading(true); - setIsInputOpen(false); - - const problemId = (document.getElementById('problem-input') as HTMLInputElement)?.value; - - assert(importChat, 'importChat is required'); - await loadProblem(problemId, importChat); - setIsLoading(false); - }; - - return ( - <> - {isInputOpen && ( - { - /* Input change handled by onKeyDown */ - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleSubmit(e as any); - } - }} - className="border border-gray-300 rounded px-2 py-1" - {...({} as any)} - /> - )} - {!isInputOpen && ( - - )} - - ); -}; diff --git a/app/components/header/Feedback.tsx b/app/components/header/Feedback.tsx index 3f8642df..f0c39f3f 100644 --- a/app/components/header/Feedback.tsx +++ b/app/components/header/Feedback.tsx @@ -1,7 +1,7 @@ import { toast } from 'react-toastify'; import ReactModal from 'react-modal'; import { useState } from 'react'; -import { supabaseSubmitFeedback } from '~/lib/supabase/problems'; +import { supabaseSubmitFeedback } from '~/lib/supabase/feedback'; import { getLastChatMessages } from '~/components/chat/Chat.client'; ReactModal.setAppElement('#root'); diff --git a/app/components/header/Header.tsx b/app/components/header/Header.tsx index 6f8365ba..0d1de169 100644 --- a/app/components/header/Header.tsx +++ b/app/components/header/Header.tsx @@ -25,6 +25,9 @@ export function Header() { logo + +
diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index ffb87c52..0e78a498 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -121,12 +121,6 @@ export const Menu = () => {
{/* Spacer for top margin */}
- - Problems - () ?? {}; + const { id: mixedId, repositoryId } = useLoaderData<{ id?: string; repositoryId?: string }>() ?? {}; const [initialMessages, setInitialMessages] = useState([]); const [resumeChat, setResumeChat] = useState(undefined); - const [ready, setReady] = useState(!mixedId && !problemId && !repositoryId); + const [ready, setReady] = useState(!mixedId && !repositoryId); const importChat = async (title: string, messages: Message[]) => { try { @@ -73,9 +68,6 @@ export function useChatHistory() { const publicData = await database.getChatPublicData(mixedId); const messages = createMessagesForRepository(publicData.title, publicData.repositoryId); await importChat(publicData.title, messages); - } else if (problemId) { - await loadProblem(problemId, importChat); - setReady(true); } else if (repositoryId) { await loadRepository(repositoryId); setReady(true); diff --git a/app/lib/replay/Problems.ts b/app/lib/replay/Problems.ts deleted file mode 100644 index 93685982..00000000 --- a/app/lib/replay/Problems.ts +++ /dev/null @@ -1,135 +0,0 @@ -// Accessors for the API to access saved problems. - -import { toast } from 'react-toastify'; -import { assert, sendCommandDedicatedClient } from './ReplayProtocolClient'; -import type { Message } from '~/lib/persistence/message'; - -// Add global declaration for the problem property -declare global { - interface Window { - __currentProblem__?: NutProblem; - } -} - -export interface NutProblemComment { - id?: string; - username?: string; - content: string; - timestamp: number; -} - -export interface NutProblemSolution { - simulationData: any; - messages: Message[]; - evaluator?: string; -} - -export enum NutProblemStatus { - // Problem has been submitted but not yet reviewed. - Pending = 'Pending', - - // Problem has been reviewed and has not been solved yet. - Unsolved = 'Unsolved', - - // Nut automatically produces a suitable explanation for solving the problem. - Solved = 'Solved', -} - -// Information about each problem stored in the index file. -export interface NutProblemDescription { - version: number; - problemId: string; - timestamp: number; - title: string; - description: string; - status?: NutProblemStatus; - keywords?: string[]; -} - -export interface NutProblem extends NutProblemDescription { - username?: string; - user_id?: string; - repositoryId: string; - comments?: NutProblemComment[]; - solution?: NutProblemSolution; -} - -export type NutProblemInput = Omit; - -export async function listAllProblems(): Promise { - let problems: NutProblemDescription[] = []; - - 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 []; - } -} - -export async function getProblem(problemId: string): Promise { - let problem: NutProblem | null = null; - - try { - if (!problemId) { - toast.error('Invalid problem ID'); - return null; - } - - const rv = await sendCommandDedicatedClient({ - method: 'Recording.globalExperimentalCommand', - params: { - name: 'fetchBoltProblem', - params: { problemId }, - }, - }); - - problem = (rv as { rval: { problem: NutProblem } }).rval.problem; - - if (!problem) { - toast.error('Problem not found'); - return null; - } - - assert(problem.repositoryId, 'Problem probably has outdated data format. Must have a repositoryId.'); - } catch (error) { - console.error('Error fetching problem', error); - - // Check for specific protocol error - if (error instanceof Error && error.message.includes('Unknown problem ID')) { - toast.error('Problem not found'); - } else { - toast.error('Failed to fetch problem'); - } - } - - /* - * Only used for testing - */ - if (problem) { - window.__currentProblem__ = problem; - } - - return problem; -} diff --git a/app/lib/supabase/feedback.ts b/app/lib/supabase/feedback.ts new file mode 100644 index 00000000..b327b671 --- /dev/null +++ b/app/lib/supabase/feedback.ts @@ -0,0 +1,30 @@ +import { getSupabase } from './client'; +import { toast } from 'react-toastify'; + +export async function supabaseSubmitFeedback(feedback: any) { + const supabase = getSupabase(); + + // Get the current user ID if available + const { + data: { user }, + } = await supabase.auth.getUser(); + const userId = user?.id || null; + + // Insert feedback into the feedback table + const { data, error } = await supabase.from('feedback').insert({ + user_id: userId, + description: feedback.description || feedback.text || JSON.stringify(feedback), + metadata: feedback, + }); + + if (error) { + console.error('Error submitting feedback to Supabase:', error); + toast.error('Failed to submit feedback'); + + return false; + } + + console.log('Feedback submitted successfully:', data); + + return true; +} diff --git a/app/lib/supabase/problems.ts b/app/lib/supabase/problems.ts deleted file mode 100644 index 7c363df6..00000000 --- a/app/lib/supabase/problems.ts +++ /dev/null @@ -1,278 +0,0 @@ -// Supabase implementation of problem management functions - -import { toast } from 'react-toastify'; -import { getSupabase, type Database } from './client'; -import type { NutProblem, NutProblemDescription, NutProblemInput, NutProblemStatus } from '~/lib/replay/Problems'; -import { getNutIsAdmin } from '~/lib/supabase/client'; - -async function downloadBlob(bucket: string, path: string) { - const supabase = getSupabase(); - const { data, error } = await supabase.storage.from(bucket).download(path); - - if (error) { - console.error('Error downloading blob:', error); - return null; - } - - return data.text(); -} - -export async function supabaseListAllProblems(): Promise { - try { - const { data, error } = await getSupabase() - .from('problems') - .select('id, created_at, updated_at, title, description, status, keywords, user_id') - .order('created_at', { ascending: false }); - - if (error) { - throw error; - } - - const problems: NutProblemDescription[] = data.map((problem) => ({ - version: 1, - problemId: problem.id, - timestamp: new Date(problem.created_at).getTime(), - title: problem.title, - description: problem.description, - status: problem.status, - keywords: problem.keywords, - })); - - return problems; - } catch (error) { - console.error('Error fetching problems', error); - toast.error('Failed to fetch problems'); - - return []; - } -} - -export async function supabaseGetProblem(problemId: string): Promise { - try { - if (!problemId) { - toast.error('Invalid problem ID'); - return null; - } - - const { data, error } = await getSupabase() - .from('problems') - .select( - ` - *, - problem_comments ( - * - ) - `, - ) - .eq('id', problemId) - .single(); - - if (error) { - // More specific error message based on error code - if (error.code === 'PGRST116') { - toast.error('Problem not found'); - } else { - toast.error(`Failed to fetch problem: ${error.message}`); - } - - throw error; - } - - if (!data) { - toast.error('Problem not found'); - return null; - } - - // Fetch blob data from storage if paths are available - let solution = data.solution; - const prompt = data.prompt; - - // Create a supabase instance for storage operations - const supabase = getSupabase(); - - if (data.solution_path) { - solution = JSON.parse((await downloadBlob('solutions', data.solution_path)) || '{}'); - } - - // If the problem has a user_id, fetch the profile information - let username = null; - - if (data.user_id) { - const { data: profileData, error: profileError } = await supabase - .from('profiles') - .select('username') - .eq('id', data.user_id) - .single(); - - if (!profileError && profileData) { - username = profileData.username; - } - } - - return { - problemId: data.id, - version: 1, - timestamp: new Date(data.created_at).getTime(), - title: data.title, - description: data.description, - status: data.status as NutProblemStatus, - keywords: data.keywords, - repositoryId: data.repository_id, - username, - solution: solution || prompt, - comments: data.problem_comments.map((comment: any) => ({ - id: comment.id, - timestamp: comment.created_at, - problemId: comment.problem_id, - content: comment.content, - username: comment.username, - })), - }; - } catch (error) { - console.error('Error fetching problem:', error); - - // Don't show duplicate toast if we already showed one above - if (!(error as any)?.code) { - toast.error('Failed to fetch problem'); - } - } - - return null; -} - -export async function supabaseSubmitProblem(problem: NutProblemInput): Promise { - try { - const supabaseProblem = { - id: undefined as any, // This will be set by Supabase - title: problem.title, - description: problem.description, - status: problem.status as NutProblemStatus, - keywords: problem.keywords || [], - repository_id: problem.repositoryId, - user_id: problem.user_id, - }; - - const { data, error } = await getSupabase().from('problems').insert(supabaseProblem).select().single(); - - if (error) { - throw error; - } - - return data.id; - } catch (error) { - console.error('Error submitting problem', error); - toast.error('Failed to submit problem'); - - return null; - } -} - -export async function supabaseDeleteProblem(problemId: string): Promise { - try { - const { error: deleteError } = await getSupabase().from('problems').delete().eq('id', problemId); - - if (deleteError) { - throw deleteError; - } - - return undefined; - } catch (error) { - console.error('Error deleting problem', error); - - return undefined; - } -} - -export async function supabaseUpdateProblem(problemId: string, problem: NutProblemInput): Promise { - try { - if (!getNutIsAdmin()) { - toast.error('Admin user required'); - return undefined; - } - - // Convert to Supabase format - const updates: Database['public']['Tables']['problems']['Update'] = { - title: problem.title, - description: problem.description, - status: problem.status, - keywords: problem.keywords || [], - repository_id: problem.repositoryId, - solution_path: problem.solution ? `solutions/${problemId}.json` : undefined, - }; - - // Update the problem - const { error: updateError } = await getSupabase().from('problems').update(updates).eq('id', problemId); - - if (updateError) { - throw updateError; - } - - if (updates.solution_path) { - const { error: solutionError } = await getSupabase() - .storage.from('solutions') - .upload(updates.solution_path, JSON.stringify(problem.solution), { upsert: true }); - - if (solutionError) { - throw solutionError; - } - } - - // Handle comments if they exist - if (problem.comments && problem.comments.length > 0) { - const commentInserts = problem.comments - .filter((comment) => !comment.id) - .map((comment) => { - return { - problem_id: problemId, - content: comment.content, - username: comment.username || 'Anonymous', - }; - }); - - /** - * Use upsert with onConflict to avoid duplicates. - * This will insert new comments and ignore existing ones based on created_at. - */ - const { error: commentsError } = await getSupabase().from('problem_comments').insert(commentInserts); - - if (commentsError) { - throw commentsError; - } - } - - return undefined; - } catch (error) { - console.error('Error updating problem', error); - toast.error('Failed to update problem'); - - return undefined; - } -} - -export async function supabaseSubmitFeedback(feedback: any) { - const supabase = getSupabase(); - - // Get the current user ID if available - const { - data: { user }, - } = await supabase.auth.getUser(); - const userId = user?.id || null; - - // Insert feedback into the feedback table - const { data, error } = await supabase.from('feedback').insert({ - user_id: userId, - description: feedback.description || feedback.text || JSON.stringify(feedback), - metadata: feedback, - }); - - if (error) { - console.error('Error submitting feedback to Supabase:', error); - toast.error('Failed to submit feedback'); - - return false; - } - - console.log('Feedback submitted successfully:', data); - - return true; -} diff --git a/app/routes/about.tsx b/app/routes/about.tsx index b9d75256..c100dd77 100644 --- a/app/routes/about.tsx +++ b/app/routes/about.tsx @@ -15,31 +15,14 @@ function AboutPage() {

About Nut

- Nut is an{' '} - - open source fork - {' '} - of{' '} - - Bolt.new - {' '} - for helping you develop full stack apps using AI. AI developers frequently struggle with fixing even simple - bugs when they don't know the cause, and get stuck making ineffective changes over and over. We want to - crack these tough nuts, so to speak, so you can get back to building. + Nut is an agentic app builder for reliably developing full stack apps using AI. + When you ask Nut to build or change an app, it will do its best to get the code + changes right the first time. Afterwards it will check the app to make sure it's + working as expected, writing tests and fixing problems those tests uncover.

- When you ask Nut to fix a bug, it creates a{' '} + You can also ask Nut to fix bugs. Nut will create a{' '}

- Nut.new is already pretty good at fixing problems, and we're working to make it better. We want it to - reliably fix anything you're seeing, as long as it has a clear explanation and the problem isn't too - complicated (AIs aren't magic). If it's doing poorly, let us know! Use the UI to leave us some private - feedback or save your project to our public set of problems where AIs struggle. -

- -

Nut is being developed by the{' '} Replay.io - {' '} - team. We're offering unlimited free access to Nut.new for early adopters who can give us feedback we'll use - to improve Nut. Reach us at{' '} - - hi@replay.io - {' '} - or fill out our{' '} + {' '} team. + We'd love to hear from you! Leave us some feedback at the top of the page, + join our{' '} - contact form + Discord {' '} - to join our early adopter program. + or reach us at {' '} + + hi@replay.io + .

diff --git a/app/routes/load-problem.$id.tsx b/app/routes/load-problem.$id.tsx deleted file mode 100644 index dd3858bc..00000000 --- a/app/routes/load-problem.$id.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { json, type LoaderFunctionArgs } from '~/lib/remix-types'; -import { default as IndexRoute } from './_index'; - -export async function loader(args: LoaderFunctionArgs) { - return json({ problemId: args.params.id }); -} - -export default IndexRoute; diff --git a/app/routes/problem.$id.tsx b/app/routes/problem.$id.tsx deleted file mode 100644 index 5fdabac2..00000000 --- a/app/routes/problem.$id.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { ClientOnly } from 'remix-utils/client-only'; -import { Header } from '~/components/header/Header'; -import { Menu } from '~/components/sidebar/Menu.client'; -import BackgroundRays from '~/components/ui/BackgroundRays'; -import { TooltipProvider } from '@radix-ui/react-tooltip'; -import { ToastContainerWrapper, Status, Keywords } from './problems'; -import { Suspense, useEffect, useState } from 'react'; -import { useParams } from '@remix-run/react'; -import { getProblem, NutProblemStatus } from '~/lib/replay/Problems'; -import type { NutProblem, NutProblemComment } from '~/lib/replay/Problems'; - -function Comments({ comments }: { comments: NutProblemComment[] }) { - return ( -
- {comments.map((comment, index) => ( -
-
- {comment.username ?? 'Anonymous'} - - {(() => { - const date = new Date(comment.timestamp); - return date && !isNaN(date.getTime()) ? date.toLocaleString() : 'Unknown date'; - })()} - -
-
{comment.content}
-
- ))} -
- ); -} - -function ProblemViewer({ problem }: { problem: NutProblem }) { - const { problemId, title, description, status = NutProblemStatus.Pending, keywords = [], comments = [] } = problem; - - return ( -
-

{title}

-

{description}

- - Load Problem - - - - -
- ); -} - -const Nothing = () => null; - -function ViewProblemPage() { - const params = useParams(); - const problemId = params.id; - - if (typeof problemId !== 'string') { - throw new Error('Problem ID is required'); - } - - const [problemData, setProblemData] = useState(null); - - useEffect(() => { - getProblem(problemId).then(setProblemData); - }, [problemId]); - - return ( - }> - -
- -
- {() => } - -
- {problemData === null ? ( -
-
-
- ) : ( - - )} -
- -
-
-
- ); -} - -export default ViewProblemPage; diff --git a/app/routes/problems.tsx b/app/routes/problems.tsx deleted file mode 100644 index 9dcd01d9..00000000 --- a/app/routes/problems.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { ClientOnly } from 'remix-utils/client-only'; -import { Header } from '~/components/header/Header'; -import { Menu } from '~/components/sidebar/Menu.client'; -import BackgroundRays from '~/components/ui/BackgroundRays'; -import { TooltipProvider } from '@radix-ui/react-tooltip'; -import { cssTransition, ToastContainer } from 'react-toastify'; -import { Suspense, useEffect, useState } from 'react'; -import { listAllProblems, NutProblemStatus } from '~/lib/replay/Problems'; -import type { NutProblemDescription } from '~/lib/replay/Problems'; - -const toastAnimation = cssTransition({ - enter: 'animated fadeInRight', - exit: 'animated fadeOutRight', -}); - -export function ToastContainerWrapper() { - return ( - { - return ( - - ); - }} - icon={({ type }) => { - /** - * @todo Handle more types if we need them. This may require extra color palettes. - */ - switch (type) { - case 'success': { - return
; - } - case 'error': { - return
; - } - } - - return undefined; - }} - position="bottom-right" - pauseOnFocusLoss - transition={toastAnimation} - /> - ); -} - -export function Status({ status }: { status: NutProblemStatus | undefined }) { - if (!status) { - status = NutProblemStatus.Pending; - } - - const statusColors: Record = { - [NutProblemStatus.Pending]: 'bg-yellow-400 dark:text-yellow-400', - [NutProblemStatus.Unsolved]: 'bg-orange-500 dark:text-orange-500', - [NutProblemStatus.Solved]: 'bg-blue-500 dark:text-blue-500', - }; - - return ( -
- Status: -
- - {status.charAt(0).toUpperCase() + status.slice(1)} -
-
- ); -} - -export function Keywords({ keywords }: { keywords: string[] | undefined }) { - if (!keywords?.length) { - return null; - } - - return ( -
- {keywords.map((keyword, index) => ( - - {keyword} - - ))} -
- ); -} - -function getProblemStatus(problem: NutProblemDescription): NutProblemStatus { - return problem.status ?? NutProblemStatus.Pending; -} - -const Nothing = () => null; - -function ProblemsPage() { - const [problems, setProblems] = useState(null); - const [statusFilter, setStatusFilter] = useState(NutProblemStatus.Solved); - - useEffect(() => { - listAllProblems().then(setProblems); - }, []); - - const filteredProblems = problems?.filter((problem) => { - return statusFilter === 'all' || getProblemStatus(problem) === statusFilter; - }); - - return ( - }> - -
- -
- {() => } - -
- {problems && ( -
- -
- )} - - {problems === null ? ( -
-
-
- ) : problems.length === 0 ? ( -
No problems found
- ) : ( - - )} -
- -
-
-
- ); -} - -export default ProblemsPage; diff --git a/tests/e2e/problem.spec.ts b/tests/e2e/problem.spec.ts deleted file mode 100644 index 598e028d..00000000 --- a/tests/e2e/problem.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { login, setLoginKey, openSidebar } from './setup/test-utils'; - -test('Should be able to load a problem', async ({ page }) => { - await page.goto('/problems'); - - const combobox = page.getByRole('combobox'); - await expect(combobox).toBeVisible({ timeout: 30000 }); - await combobox.selectOption('all'); - - const problemLink = page.getByRole('link', { name: 'Contact book tiny search icon' }).first(); - await expect(problemLink).toBeVisible({ timeout: 30000 }); - await problemLink.click(); - - const loadProblemLink = page.getByRole('link', { name: 'Load Problem' }); - await expect(loadProblemLink).toBeVisible({ timeout: 30000 }); - await loadProblemLink.click(); - - await expect(page.getByText('Import the "problem" folder')).toBeVisible({ timeout: 30000 }); -}); - -// TODO: Unskip this test once we can make sure we get a repro for a problem. -test.skip('Should be able to save a problem ', async ({ page }) => { - await page.goto('/problems'); - await page.getByRole('link', { name: 'App goes blank getting' }).click(); - await page.getByRole('link', { name: 'Load Problem' }).click(); - - await expect(page.getByText('Import the "problem" folder')).toBeVisible({ timeout: 30000 }); - await login(page); - - await expect(page.getByText('Import the "problem" folder')).toBeVisible({ timeout: 30000 }); - - await openSidebar(page); - await page.getByRole('button', { name: 'Save Problem' }).click(); - - await page.locator('input[name="title"]').click(); - await page.locator('input[name="title"]').fill('[test] playwright'); - await page.locator('input[name="description"]').click(); - await page.locator('input[name="description"]').fill('...'); - - await page.getByRole('button', { name: 'Submit' }).click(); - 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(); - - await login(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(); -}); - -test('Should be able to add a comment to 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(); - - await login(page); - - // Add a comment to the problem - const comment = `test comment ${Date.now().toString()}`; - await page.getByRole('textbox', { name: 'Add a comment...' }).click(); - await page.getByRole('textbox', { name: 'Add a comment...' }).fill(comment); - await page.getByRole('button', { name: 'Add Comment' }).click(); - await expect(page.locator('[data-testid="problem-comment"]').filter({ hasText: comment })).toBeVisible(); - - // Reload the page and check that the comment is still visible - await page.reload(); - await expect(page.locator('[data-testid="problem-comment"]').filter({ hasText: comment })).toBeVisible(); -}); - -test('Confirm that admins see the "Save Problem" button', async ({ page }) => { - await page.goto('/problems?showAll=true'); - - await login(page); - await openSidebar(page); - await expect(page.getByRole('button', { name: 'Save Problem' })).toBeVisible(); -});