2 Commits

Author SHA1 Message Date
Jayden Pyles
6d45bd129c chore: refactor wip 2025-05-31 14:26:50 -05:00
Jayden Pyles
3ab31bd186 chore: refactor wip 2025-05-31 14:21:51 -05:00
11 changed files with 205 additions and 36 deletions

View File

@@ -0,0 +1,80 @@
import { login } from "../utilities/authentication.utils";
import {
addCustomHeaders,
addSiteMapAction,
cleanUpJobs,
mockSubmitJob,
submitBasicJob,
waitForJobCompletion,
} from "../utilities/job.utilities";
describe.only("Advanced Job Options", () => {
beforeEach(() => {
mockSubmitJob();
login();
cy.visit("/");
});
afterEach(() => {
cleanUpJobs();
});
it.only("should handle custom headers", () => {
const customHeaders = {
"User-Agent": "Test Agent",
"Accept-Language": "en-US",
};
addCustomHeaders(customHeaders);
submitBasicJob("https://httpbin.org/headers", "headers", "//pre");
cy.wait("@submitScrapeJob").then((interception) => {
expect(interception.response?.statusCode).to.eq(200);
expect(
interception.request?.body.data.job_options.custom_headers
).to.deep.equal(customHeaders);
});
cy.get("li").contains("Jobs").click();
waitForJobCompletion("https://httpbin.org/headers");
});
it("should handle site map actions", () => {
addSiteMapAction("click", "//button[contains(text(), 'Load More')]");
addSiteMapAction("input", "//input[@type='search']", "test search");
submitBasicJob("https://example.com", "content", "//div[@class='content']");
cy.wait("@submitScrapeJob").then((interception) => {
expect(interception.response?.statusCode).to.eq(200);
const siteMap = interception.request?.body.data.job_options.site_map;
expect(siteMap.actions).to.have.length(2);
expect(siteMap.actions[0].type).to.equal("click");
expect(siteMap.actions[1].type).to.equal("input");
});
cy.get("li").contains("Jobs").click();
waitForJobCompletion("https://example.com");
});
it("should handle multiple elements", () => {
cy.get('[data-cy="url-input"]').type("https://books.toscrape.com");
cy.get('[data-cy="name-field"]').type("titles");
cy.get('[data-cy="xpath-field"]').type("//h3");
cy.get('[data-cy="add-button"]').click();
cy.get('[data-cy="name-field"]').type("prices");
cy.get('[data-cy="xpath-field"]').type("//p[@class='price_color']");
cy.get('[data-cy="add-button"]').click();
cy.contains("Submit").click();
cy.wait("@submitScrapeJob").then((interception) => {
expect(interception.response?.statusCode).to.eq(200);
expect(interception.request?.body.data.elements).to.have.length(2);
});
cy.get("li").contains("Jobs").click();
waitForJobCompletion("https://books.toscrape.com");
});
});

View File

@@ -7,3 +7,54 @@ export const cleanUpJobs = () => {
cy.get("[data-testid='DeleteIcon']").click();
};
export const submitBasicJob = (url: string, name: string, xpath: string) => {
cy.get('[data-cy="url-input"]').type(url);
cy.get('[data-cy="name-field"]').type(name);
cy.get('[data-cy="xpath-field"]').type(xpath);
cy.get('[data-cy="add-button"]').click();
cy.contains("Submit").click();
};
export const waitForJobCompletion = (url: string) => {
cy.contains("div", url, { timeout: 10000 }).should("exist");
cy.contains("div", "Completed", { timeout: 20000 }).should("exist");
};
export const enableMultiPageScraping = () => {
cy.get("button").contains("Advanced Job Options").click();
cy.get('[data-cy="multi-page-toggle"]').click();
cy.get("body").type("{esc}");
};
export const addCustomHeaders = (headers: Record<string, string>) => {
cy.get("button").contains("Advanced Job Options").click();
cy.get('[name="custom_headers"]').type(JSON.stringify(headers), {
parseSpecialCharSequences: false,
});
cy.get("body").type("{esc}");
};
export const addCustomCookies = (cookies: Record<string, string>) => {
cy.get("button").contains("Advanced Job Options").click();
cy.get('[name="custom_cookies"]').type(JSON.stringify(cookies));
cy.get("body").type("{esc}");
};
export const addSiteMapAction = (
type: "click" | "input",
xpath: string,
input?: string
) => {
cy.get("button").contains("Create Site Map").click();
cy.get('[data-cy="site-map-select"]').select(type);
cy.get('[data-cy="site-map-xpath"]').type(xpath);
if (type === "input" && input) {
cy.get('[data-cy="site-map-input"]').type(input);
}
cy.get('[data-cy="add-site-map-action"]').click();
};
export const mockSubmitJob = () => {
cy.intercept("POST", "/api/submit-scrape-job").as("submitScrapeJob");
};

View File

@@ -36,6 +36,7 @@
"react-router": "^6.14.1",
"react-router-dom": "^6.14.1",
"react-spinners": "^0.14.1",
"react-toastify": "^11.0.5",
"redux-persist": "^6.0.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"

View File

@@ -25,7 +25,7 @@ import {
Typography,
useTheme,
} from "@mui/material";
import { Dispatch, SetStateAction } from "react";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
export type AdvancedJobOptionsDialogProps = {
open: boolean;
@@ -43,31 +43,45 @@ export const AdvancedJobOptionsDialog = ({
multiPageScrapeEnabled = true,
}: AdvancedJobOptionsDialogProps) => {
const theme = useTheme();
const [localJobOptions, setLocalJobOptions] =
useState<RawJobOptions>(jobOptions);
// Update local state when prop changes
useEffect(() => {
setLocalJobOptions(jobOptions);
}, [jobOptions]);
const handleMultiPageScrapeChange = () => {
setJobOptions((prevJobOptions) => ({
setLocalJobOptions((prevJobOptions) => ({
...prevJobOptions,
multi_page_scrape: !prevJobOptions.multi_page_scrape,
}));
};
const handleProxiesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setJobOptions((prevJobOptions) => ({
setLocalJobOptions((prevJobOptions) => ({
...prevJobOptions,
proxies: e.target.value,
}));
};
const handleCollectMediaChange = () => {
setJobOptions((prevJobOptions) => ({
setLocalJobOptions((prevJobOptions) => ({
...prevJobOptions,
collect_media: !prevJobOptions.collect_media,
}));
};
const handleClose = () => {
// Save the local state back to the parent before closing
setJobOptions(localJobOptions);
onClose();
};
return (
<Dialog
open={open}
onClose={onClose}
onClose={handleClose}
maxWidth="md"
fullWidth
PaperProps={{
@@ -122,7 +136,7 @@ export const AdvancedJobOptionsDialog = ({
<FormControlLabel
control={
<Checkbox
checked={jobOptions.multi_page_scrape}
checked={localJobOptions.multi_page_scrape}
onChange={handleMultiPageScrapeChange}
disabled={!multiPageScrapeEnabled}
/>
@@ -147,7 +161,7 @@ export const AdvancedJobOptionsDialog = ({
<FormControlLabel
control={
<Checkbox
checked={jobOptions.collect_media}
checked={localJobOptions.collect_media}
onChange={handleCollectMediaChange}
data-cy="collect-media-checkbox"
/>
@@ -233,7 +247,7 @@ export const AdvancedJobOptionsDialog = ({
fullWidth
variant="outlined"
size="small"
value={jobOptions.proxies}
value={localJobOptions.proxies}
onChange={handleProxiesChange}
InputProps={{
startAdornment: (
@@ -251,8 +265,9 @@ export const AdvancedJobOptionsDialog = ({
label="Custom Headers"
placeholder='{"User-Agent": "CustomAgent", "Accept": "*/*"}'
urlParam="custom_headers"
name="custom_headers"
onChange={(value) => {
setJobOptions((prevJobOptions) => ({
setLocalJobOptions((prevJobOptions) => ({
...prevJobOptions,
custom_headers: value,
}));
@@ -264,8 +279,9 @@ export const AdvancedJobOptionsDialog = ({
label="Custom Cookies"
placeholder='[{"name": "value", "name2": "value2"}]'
urlParam="custom_cookies"
name="custom_cookies"
onChange={(value) => {
setJobOptions((prevJobOptions) => ({
setLocalJobOptions((prevJobOptions) => ({
...prevJobOptions,
custom_cookies: value,
}));

View File

@@ -23,6 +23,7 @@ export type ExpandedTableInputProps = {
onChange: (value: any) => void;
placeholder: string;
urlParam: string;
name: string;
};
export const ExpandedTableInput = ({
@@ -30,6 +31,7 @@ export const ExpandedTableInput = ({
onChange,
placeholder,
urlParam,
name,
}: ExpandedTableInputProps) => {
const theme = useTheme();
const [value, setValue] = useState("");
@@ -150,6 +152,7 @@ export const ExpandedTableInput = ({
size="small"
error={jsonError !== null}
helperText={jsonError ?? ""}
name={name}
/>
{parsedHeaders && parsedHeaders.length > 0 && (

View File

@@ -1,7 +1,7 @@
.welcome {
margin: 0.25rem;
margin: 0.5rem !important;
}
.userControlButton {
width: 100%;
}
}

View File

@@ -1,15 +1,14 @@
"use client";
import { AdvancedJobOptions } from "@/components/common/advanced-job-options";
import { useSubmitJob } from "@/hooks/use-submit-job";
import { parseJobOptions } from "@/lib";
import { initialJobOptions, RawJobOptions } from "@/types/job";
import { useUser } from "@/store/hooks";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { JobSubmitterHeader } from "./job-submitter-header";
import { JobSubmitterInput } from "./job-submitter-input";
import { useJobSubmitterProvider } from "./provider";
import { useUser } from "@/store/hooks";
import { useSubmitJob } from "@/hooks/use-submit-job";
export const JobSubmitter = () => {
const router = useRouter();
@@ -17,10 +16,8 @@ export const JobSubmitter = () => {
const { user } = useUser();
const { submitJob, loading, error } = useSubmitJob();
const { submittedURL, rows, siteMap, setSiteMap } = useJobSubmitterProvider();
const [jobOptions, setJobOptions] =
useState<RawJobOptions>(initialJobOptions);
const { submittedURL, rows, siteMap, setSiteMap, jobOptions, setJobOptions } =
useJobSubmitterProvider();
useEffect(() => {
if (job_options) {
@@ -32,6 +29,11 @@ export const JobSubmitter = () => {
await submitJob(submittedURL, rows, user, jobOptions, siteMap, false, null);
};
console.log(jobOptions);
useEffect(() => {
console.log(jobOptions);
}, [jobOptions]);
return (
<div>
<JobSubmitterHeader />

View File

@@ -1,12 +1,13 @@
import { Element, RawJobOptions, Result, SiteMap } from "@/types";
import { initialJobOptions } from "@/types/job";
import React, {
createContext,
Dispatch,
PropsWithChildren,
useContext,
useState,
Dispatch,
useMemo,
useState,
} from "react";
import { Element, Result, SiteMap } from "@/types";
type JobSubmitterProviderType = {
submittedURL: string;
@@ -25,6 +26,8 @@ type JobSubmitterProviderType = {
setIsValidUrl: Dispatch<React.SetStateAction<boolean>>;
siteMap: SiteMap | null;
setSiteMap: Dispatch<React.SetStateAction<SiteMap | null>>;
jobOptions: RawJobOptions;
setJobOptions: Dispatch<React.SetStateAction<RawJobOptions>>;
closeSnackbar: () => void;
};
@@ -41,6 +44,8 @@ export const Provider = ({ children }: PropsWithChildren) => {
const [snackbarSeverity, setSnackbarSeverity] = useState<string>("error");
const [isValidURL, setIsValidUrl] = useState<boolean>(true);
const [siteMap, setSiteMap] = useState<SiteMap | null>(null);
const [jobOptions, setJobOptions] =
useState<RawJobOptions>(initialJobOptions);
const closeSnackbar = () => {
setSnackbarOpen(false);
@@ -66,6 +71,8 @@ export const Provider = ({ children }: PropsWithChildren) => {
setIsValidUrl,
siteMap,
setSiteMap,
jobOptions,
setJobOptions,
closeSnackbar,
}),
[
@@ -77,6 +84,7 @@ export const Provider = ({ children }: PropsWithChildren) => {
snackbarSeverity,
isValidURL,
siteMap,
jobOptions,
closeSnackbar,
]
);

View File

@@ -1,15 +1,16 @@
import "bootstrap/dist/css/bootstrap.min.css";
import "@/styles/globals.css";
import "bootstrap/dist/css/bootstrap.min.css";
import React, { useState, useEffect } from "react";
import { NavDrawer } from "@/components/common";
import { persistor, store } from "@/store/store";
import { darkTheme, lightTheme } from "@/styles/themes";
import { Box, CssBaseline, ThemeProvider } from "@mui/material";
import type { AppProps } from "next/app";
import Head from "next/head";
import { ThemeProvider, CssBaseline, Box } from "@mui/material";
import { NavDrawer } from "@/components/common";
import { darkTheme, lightTheme } from "@/styles/themes";
import React, { useEffect, useState } from "react";
import { Provider } from "react-redux";
import { ToastContainer } from "react-toastify";
import { PersistGate } from "redux-persist/integration/react";
import { store, persistor } from "@/store/store";
const App: React.FC<AppProps> = ({ Component, pageProps }) => {
const [isDarkMode, setIsDarkMode] = useState(false);
@@ -56,6 +57,7 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
<Component {...pageProps} />
</Box>
</Box>
<ToastContainer theme={isDarkMode ? "dark" : "light"} />
</ThemeProvider>
</PersistGate>
</Provider>

View File

@@ -1,13 +1,14 @@
"use client";
import { ApiService } from "@/services";
import { useUser, useUserSettings } from "@/store/hooks";
import { Box, Button, TextField, Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import axios from "axios";
import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { getUserSettings } from "../lib";
import { ApiService } from "@/services";
type Mode = "login" | "signup";
@@ -37,9 +38,6 @@ const AuthForm: React.FC = () => {
if (mode === "login") {
const user = await ApiService.login(email, password);
console.log(user);
alert("Login successful");
const userSettings = await getUserSettings();
setUserSettings(userSettings);
@@ -52,15 +50,16 @@ const AuthForm: React.FC = () => {
error: null,
});
toast.success("Login successful");
router.push("/");
} else {
await ApiService.register(email, password, fullName);
alert("Signup successful");
router.push("/login");
setMode("login");
toast.success("Registration successful");
}
} catch (error) {
console.error(error);
alert(`${mode.charAt(0).toUpperCase() + mode.slice(1)} failed`);
toast.error("There was an error logging or registering");
}
};

View File

@@ -4765,6 +4765,13 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
get-nonce "^1.0.0"
tslib "^2.0.0"
react-toastify@^11.0.5:
version "11.0.5"
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-11.0.5.tgz#ce4c42d10eeb433988ab2264d3e445c4e9d13313"
integrity sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==
dependencies:
clsx "^2.1.1"
react-transition-group@^4.4.5:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"