diff --git a/cypress.config.js b/cypress.config.js
index 808827b..705ec08 100644
--- a/cypress.config.js
+++ b/cypress.config.js
@@ -1,8 +1,10 @@
import { defineConfig } from "cypress";
export default defineConfig({
+ viewportHeight: 1080,
+ viewportWidth: 1920,
e2e: {
- baseUrl: "https://reactive-api-console.vercel.app/",
+ baseUrl: "http://localhost:5173/", //"https://reactive-api-console.vercel.app/"
supportFile: "cypress/support/e2e.{js,jsx,ts,tsx}",
},
component: {
diff --git a/cypress/e2e/api-should-fail.cy.ts b/cypress/e2e/api-should-fail.cy.ts
new file mode 100644
index 0000000..2bf02da
--- /dev/null
+++ b/cypress/e2e/api-should-fail.cy.ts
@@ -0,0 +1,43 @@
+///
+
+describe("API Failing Cases", () => {
+ beforeEach(() => {
+ cy.visit("/");
+ });
+
+ it("should show error for unsupported command", () => {
+ cy.get('input[placeholder*="Type a command"]').type("unsupported command");
+ cy.get("button").contains("Send").click();
+ cy.contains("No results for").should("be.visible");
+ });
+
+ it("should show error if command is valid but API is disabled", () => {
+ // Assume Cat Facts is enabled by default, so we disable it first
+ cy.contains("Cat Facts").click(); // This should disable the API
+ cy.contains("Cat Facts").parent().not("have.class", "ring-orange-500");
+ cy.get('input[placeholder*="Type a command"]').type("get cat fact");
+ cy.get("button").contains("Send").click();
+ cy.contains("No results for").should("be.visible");
+ });
+
+ it("should show error if network/API is down (network failure)", () => {
+ cy.intercept("GET", "https://catfact.ninja/fact", {
+ forceNetworkError: true,
+ }).as("getCatFactFail");
+ cy.get('input[placeholder*="Type a command"]').type("get cat fact");
+ cy.get("button").contains("Send").click();
+ cy.wait("@getCatFactFail");
+ cy.contains("Network error").should("be.visible");
+ });
+
+ it("should show error if API returns 500 error", () => {
+ cy.intercept("GET", "https://catfact.ninja/fact", {
+ statusCode: 500,
+ body: {},
+ }).as("getCatFact500");
+ cy.get('input[placeholder*="Type a command"]').type("get cat fact");
+ cy.get("button").contains("Send").click();
+ cy.wait("@getCatFact500");
+ cy.contains("Error executing").should("be.visible");
+ });
+});
diff --git a/cypress/e2e/special-commands/clear-commands.cy.ts b/cypress/e2e/special-commands/clear-commands.cy.ts
new file mode 100644
index 0000000..b30c0db
--- /dev/null
+++ b/cypress/e2e/special-commands/clear-commands.cy.ts
@@ -0,0 +1,16 @@
+///
+
+describe("Clear Commands", () => {
+ it("should clear chat when 'clear' command is executed", () => {
+ cy.visit("/");
+ // Execute a command to populate chat
+ cy.get('input[placeholder*="Type a command"]').type("get cat fact");
+ cy.get("button").contains("Send").click();
+ cy.contains("✅ Executed: get cat fact").should("be.visible");
+
+ cy.get('input[placeholder*="Type a command"]').type("clear");
+ cy.get("button").contains("Send").click();
+ // The chat should be cleared, so the executed message should not be visible
+ cy.contains("✅ Executed: get cat fact").should("not.exist");
+ });
+});
diff --git a/cypress/e2e/special-commands/help-command.cy.ts b/cypress/e2e/special-commands/help-command.cy.ts
new file mode 100644
index 0000000..7171e8a
--- /dev/null
+++ b/cypress/e2e/special-commands/help-command.cy.ts
@@ -0,0 +1,14 @@
+///
+
+describe("Help Commands", () => {
+ beforeEach(() => {
+ cy.visit("/");
+ });
+
+ it("should display help information when 'help' command is executed", () => {
+ cy.get('input[placeholder*="Type a command"]').type("help");
+ cy.get("button").should("not.be.disabled");
+ cy.get("button").contains("Send").click();
+ cy.contains("Available Commands").should("be.visible");
+ });
+});
diff --git a/cypress/e2e/special-commands/history-command.cy.ts b/cypress/e2e/special-commands/history-command.cy.ts
new file mode 100644
index 0000000..086871e
--- /dev/null
+++ b/cypress/e2e/special-commands/history-command.cy.ts
@@ -0,0 +1,31 @@
+///
+
+describe("History Commands", () => {
+ beforeEach(() => {
+ cy.visit("/");
+ });
+
+ it("should show No history when 'history' command is executed without any history", () => {
+ // check history
+ cy.get('input[placeholder*="Type a command"]').type("history");
+ cy.get("button").contains("Send").click();
+ cy.contains("No command").should("be.visible");
+ });
+
+ it("should show command history when 'history' command is executed", () => {
+ // Execute a command to ensure there is history
+ cy.get('input[placeholder*="Type a command"]').type("get chuck joke");
+ cy.get("button").contains("Send").click();
+
+ cy.get('input[placeholder*="Type a command"]').type("history");
+ cy.get("button").contains("Send").click();
+
+ cy.get('input[placeholder*="Type a command"]').type("get activity");
+ cy.get("button").contains("Send").click();
+
+ // Now check history
+ cy.get('input[placeholder*="Type a command"]').type("history");
+ cy.get("button").contains("Send").click();
+ cy.contains("Command History").should("be.visible");
+ });
+});
diff --git a/package.json b/package.json
index df7de18..959e567 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
- "dev": "vite",
+ "dev": "vite --port 5173",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
@@ -13,7 +13,8 @@
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage.enabled --coverage.reportsDirectory ./coverage",
"cy:open": "cypress open",
- "cy:run": "cypress run"
+ "cy:run": "cypress run",
+ "cy:start": "concurrently \"pnpm run dev\" \"cypress run\""
},
"dependencies": {
"@reduxjs/toolkit": "^2.8.2",
@@ -46,6 +47,7 @@
"react-dom": "^19.1.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
+ "concurrently": "9.2.0",
"vite": "^6.3.5",
"vitest": "^3.2.4"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d5f8b71..a910041 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -60,6 +60,9 @@ importers:
'@vitest/ui':
specifier: 3.2.4
version: 3.2.4(vitest@3.2.4)
+ concurrently:
+ specifier: 9.2.0
+ version: 9.2.0
cypress:
specifier: ^14.5.1
version: 14.5.1
@@ -1077,6 +1080,10 @@ packages:
resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==}
engines: {node: '>=8'}
+ cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -1106,6 +1113,11 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ concurrently@9.2.0:
+ resolution: {integrity: sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
@@ -1409,6 +1421,10 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@@ -2019,6 +2035,10 @@ packages:
request-progress@3.0.0:
resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==}
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
@@ -2074,6 +2094,10 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
+ shell-quote@1.8.3:
+ resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
+ engines: {node: '>= 0.4'}
+
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
@@ -2460,6 +2484,10 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@@ -2467,6 +2495,14 @@ packages:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
+ yargs@17.7.2:
+ resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
+ engines: {node: '>=12'}
+
yauzl@2.10.0:
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
@@ -3417,6 +3453,12 @@ snapshots:
slice-ansi: 3.0.0
string-width: 4.2.3
+ cliui@8.0.1:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -3438,6 +3480,16 @@ snapshots:
concat-map@0.0.1: {}
+ concurrently@9.2.0:
+ dependencies:
+ chalk: 4.1.2
+ lodash: 4.17.21
+ rxjs: 7.8.2
+ shell-quote: 1.8.3
+ supports-color: 8.1.1
+ tree-kill: 1.2.2
+ yargs: 17.7.2
+
convert-source-map@2.0.0: {}
core-util-is@1.0.2: {}
@@ -3814,6 +3866,8 @@ snapshots:
gensync@1.0.0-beta.2: {}
+ get-caller-file@2.0.5: {}
+
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -4351,6 +4405,8 @@ snapshots:
dependencies:
throttleit: 1.0.1
+ require-directory@2.1.1: {}
+
reselect@5.1.1: {}
resolve-from@4.0.0: {}
@@ -4414,6 +4470,8 @@ snapshots:
shebang-regex@3.0.0: {}
+ shell-quote@1.8.3: {}
+
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
@@ -4771,10 +4829,24 @@ snapshots:
wrappy@1.0.2: {}
+ y18n@5.0.8: {}
+
yallist@3.1.1: {}
yallist@5.0.0: {}
+ yargs-parser@21.1.1: {}
+
+ yargs@17.7.2:
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
+
yauzl@2.10.0:
dependencies:
buffer-crc32: 0.2.13
diff --git a/src/components/molecules/ChatCommand/ChatCommand.tsx b/src/components/molecules/ChatCommand/ChatCommand.tsx
index 886b092..c74245a 100644
--- a/src/components/molecules/ChatCommand/ChatCommand.tsx
+++ b/src/components/molecules/ChatCommand/ChatCommand.tsx
@@ -1,34 +1,32 @@
import { useEffect, useState } from "react";
-import { BehaviorSubject, debounceTime, tap } from "rxjs";
+import { debounceTime, BehaviorSubject as Subject, tap } from "rxjs";
type ChatCommandProps = {
onSendCommand: (command: string) => void;
};
-const messageChange = new BehaviorSubject("");
+const messageChange = new Subject("");
const messageChange$ = messageChange.asObservable();
export const ChatCommand = ({ onSendCommand }: ChatCommandProps) => {
- const [command, setCommand] = useState("");
-
+ const [disabled, setDisabled] = useState(true);
useEffect(() => {
- const subscription = messageChange$
+ const subscription$ = messageChange$
.pipe(
debounceTime(500),
- tap((value) => {
- setCommand(value);
- })
+ tap((value) => setDisabled(!value.trim()))
)
.subscribe();
- return () => subscription.unsubscribe();
+ return () => subscription$.unsubscribe();
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
- if (command.trim()) {
- onSendCommand(command.trim());
- messageChange.next("");
+
+ if (messageChange.value.trim()) {
+ onSendCommand(messageChange.value.trim());
(e.target as HTMLFormElement).reset();
+ setDisabled(true);
}
};
@@ -45,7 +43,7 @@ export const ChatCommand = ({ onSendCommand }: ChatCommandProps) => {
/>