Skip to content

Commit 8bcd9eb

Browse files
refactor: streamline JobsFiltersCard tests with render helper and improve clarity
test: update JobsTableCard tests for improved assertions and structure refactor: enhance useJobsFiltering tests for better readability and functionality test: optimize JobsPage tests with clearer structure and additional assertions refactor: improve useJobsPagination tests for better coverage and clarity
1 parent 60e7bc3 commit 8bcd9eb

13 files changed

Lines changed: 855 additions & 509 deletions

File tree

frontend/src/domains/jobs/application/useJobsData.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
import { fetchJobFiles, fetchJobsByFile, runScraperRequest } from "@/domains/jobs/infrastructure/jobsApi";
1+
import {
2+
fetchJobFiles,
3+
fetchJobsByFile,
4+
runScraperRequest,
5+
} from "@/domains/jobs/infrastructure/jobsApi";
26
import type { Job, JobFile, JobsMeta } from "@/domains/jobs/domain/job.types";
37
import { useCallback, useEffect, useState } from "react";
48

59
const EMPTY_META: JobsMeta = { file: "", modifiedAt: null, total: 0 };
610

7-
export function useJobsData() {
11+
export function useJobsData(initialSelectedFile = "") {
812
const [files, setFiles] = useState<JobFile[]>([]);
9-
const [selectedFile, setSelectedFile] = useState("");
13+
const [selectedFile, setSelectedFile] = useState(initialSelectedFile);
1014
const [jobs, setJobs] = useState<Job[]>([]);
1115
const [meta, setMeta] = useState<JobsMeta>(EMPTY_META);
1216
const [loading, setLoading] = useState(false);
@@ -38,7 +42,11 @@ export function useJobsData() {
3842
} catch (err: unknown) {
3943
setJobs([]);
4044
setMeta(EMPTY_META);
41-
setError(err instanceof Error ? err.message : "Erro inesperado ao carregar vagas.");
45+
setError(
46+
err instanceof Error
47+
? err.message
48+
: "Erro inesperado ao carregar vagas.",
49+
);
4250
} finally {
4351
setLoading(false);
4452
}
@@ -48,7 +56,7 @@ export function useJobsData() {
4856
async function initializeFiles() {
4957
const foundFiles = await loadFiles();
5058
if (foundFiles[0]?.file) {
51-
setSelectedFile(foundFiles[0].file);
59+
setSelectedFile((current) => current || foundFiles[0].file);
5260
}
5361
}
5462

@@ -94,7 +102,11 @@ export function useJobsData() {
94102

95103
await loadJobs(nextFile);
96104
} catch (err: unknown) {
97-
setError(err instanceof Error ? err.message : "Erro inesperado ao executar o scraper.");
105+
setError(
106+
err instanceof Error
107+
? err.message
108+
: "Erro inesperado ao executar o scraper.",
109+
);
98110
} finally {
99111
setScraping(false);
100112
}

frontend/src/domains/jobs/application/useJobsFiltering.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import type { Job } from "@/domains/jobs/domain/job.types";
2-
import { dedupeJobs, filterJobs, getAvailableKeywords } from "@/domains/jobs/domain/jobFilters";
3-
import { useMemo, useState } from "react";
4-
5-
export function useJobsFiltering(jobs: Job[]) {
6-
const [search, setSearch] = useState("");
7-
const [keywordFilter, setKeywordFilter] = useState<string[]>([]);
2+
import {
3+
dedupeJobs,
4+
filterJobs,
5+
getAvailableKeywords,
6+
} from "@/domains/jobs/domain/jobFilters";
7+
import { useMemo } from "react";
88

9+
export function useJobsFiltering(
10+
jobs: Job[],
11+
search = "",
12+
keywordFilter: string[] = [],
13+
) {
914
const dedupedJobs = useMemo(() => dedupeJobs(jobs), [jobs]);
1015

11-
const keywords = useMemo(() => getAvailableKeywords(dedupedJobs), [dedupedJobs]);
16+
const keywords = useMemo(
17+
() => getAvailableKeywords(dedupedJobs),
18+
[dedupedJobs],
19+
);
1220

1321
const filteredJobs = useMemo(
1422
() => filterJobs(dedupedJobs, search, keywordFilter),
@@ -17,9 +25,7 @@ export function useJobsFiltering(jobs: Job[]) {
1725

1826
return {
1927
search,
20-
setSearch,
2128
keywordFilter,
22-
setKeywordFilter,
2329
keywords,
2430
filteredJobs,
2531
};

frontend/src/domains/jobs/domain/jobFilters.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,17 @@ export function dedupeJobs(jobs: Job[]) {
8686
continue;
8787
}
8888

89-
const mergedKeywords = [...new Set([...splitJobKeywords(existing), ...splitJobKeywords(job)])];
89+
const mergedKeywords = [
90+
...new Set([...splitJobKeywords(existing), ...splitJobKeywords(job)]),
91+
];
9092
const mergedSources = [
9193
...new Set(
92-
[...(existing.sources ?? []), ...(job.sources ?? []), existing.source, job.source]
94+
[
95+
...(existing.sources ?? []),
96+
...(job.sources ?? []),
97+
existing.source,
98+
job.source,
99+
]
93100
.map((source) => String(source || "").trim())
94101
.filter(Boolean),
95102
),
@@ -113,12 +120,25 @@ export function dedupeJobs(jobs: Job[]) {
113120
}
114121

115122
export function getAvailableKeywords(jobs: Job[]) {
116-
const values = Array.from(new Set(jobs.flatMap((job) => splitJobKeywords(job))));
123+
const values = Array.from(
124+
new Set(jobs.flatMap((job) => splitJobKeywords(job))),
125+
);
117126
return values.sort((a, b) => a.localeCompare(b));
118127
}
119128

120-
export function filterJobs(jobs: Job[], search: string, keywordFilter: string[]) {
121-
const term = normalizeComparableText(search);
129+
function getSearchTerms(search: string) {
130+
return search
131+
.split(/[,;/]+/)
132+
.map((term) => normalizeComparableText(term))
133+
.filter(Boolean);
134+
}
135+
136+
export function filterJobs(
137+
jobs: Job[],
138+
search: string,
139+
keywordFilter: string[],
140+
) {
141+
const terms = getSearchTerms(search);
122142

123143
return jobs.filter((job) => {
124144
const currentKeywords = splitJobKeywords(job);
@@ -130,14 +150,21 @@ export function filterJobs(jobs: Job[], search: string, keywordFilter: string[])
130150
return false;
131151
}
132152

133-
if (!term) {
153+
if (terms.length === 0) {
134154
return true;
135155
}
136156

137157
const text = normalizeComparableText(
138-
[job.titulo, job.empresa, job.local, job.link, job.palavra, ...(job.keywords || [])].join(" "),
158+
[
159+
job.titulo,
160+
job.empresa,
161+
job.local,
162+
job.link,
163+
job.palavra,
164+
...(job.keywords || []),
165+
].join(" "),
139166
);
140167

141-
return text.includes(term);
168+
return terms.every((term) => text.includes(term));
142169
});
143170
}

frontend/src/domains/jobs/presentation/components/JobsFiltersCard.tsx

Lines changed: 83 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,33 @@ import { Badge } from "@/shared/ui/badge";
22
import { Card, CardContent } from "@/shared/ui/card";
33
import { Input } from "@/shared/ui/input";
44
import type { JobFile, JobsMeta } from "@/domains/jobs/domain/job.types";
5-
import { useMemo, useState, type Dispatch, type ReactNode, type SetStateAction } from "react";
6-
import { FiBriefcase, FiCheck, FiFileText, FiFilter, FiSearch, FiSlash, FiTag } from "react-icons/fi";
5+
import {
6+
useMemo,
7+
useState,
8+
type FormEvent,
9+
type Dispatch,
10+
type ReactNode,
11+
type SetStateAction,
12+
} from "react";
13+
import {
14+
FiBriefcase,
15+
FiCheck,
16+
FiFileText,
17+
FiFilter,
18+
FiPlus,
19+
FiSearch,
20+
FiSlash,
21+
FiTag,
22+
} from "react-icons/fi";
723
import { KeywordsModal } from "./KeywordsModal";
824
import { Button } from "@/shared/ui/button";
925
interface JobsFiltersCardProps {
1026
search: string;
1127
setSearch: Dispatch<SetStateAction<string>>;
1228
keywordFilter: string[];
1329
setKeywordFilter: Dispatch<SetStateAction<string[]>>;
30+
onRemoveFilter: (filterToRemove: string) => void;
31+
onClearFilters: () => void;
1432
keywords: string[];
1533
selectedFile: string;
1634
setSelectedFile: Dispatch<SetStateAction<string>>;
@@ -23,17 +41,20 @@ function getSelectedFilters(search: string, keywordFilter: string[]) {
2341
const searchTerms = search
2442
.split(/[,;/]+/)
2543
.map((item) => item.trim())
26-
.filter(Boolean)
27-
.slice(0, 5);
44+
.filter(Boolean);
2845

29-
return Array.from(new Set([...keywordFilter.filter(Boolean), ...searchTerms]));
46+
return Array.from(
47+
new Set([...keywordFilter.filter(Boolean), ...searchTerms]),
48+
);
3049
}
3150

3251
export function JobsFiltersCard({
3352
search,
3453
setSearch,
3554
keywordFilter,
3655
setKeywordFilter,
56+
onRemoveFilter,
57+
onClearFilters,
3758
keywords,
3859
selectedFile,
3960
setSelectedFile,
@@ -42,8 +63,12 @@ export function JobsFiltersCard({
4263
actions,
4364
}: JobsFiltersCardProps) {
4465
const [seeKeywordsModal, setSeeKeywordsModal] = useState(false);
66+
const [searchTerm, setSearchTerm] = useState("");
4567

46-
const selectedFilters = useMemo(() => getSelectedFilters(search, keywordFilter), [search, keywordFilter]);
68+
const selectedFilters = useMemo(
69+
() => getSelectedFilters(search, keywordFilter),
70+
[search, keywordFilter],
71+
);
4772

4873
const handleManageFilters = () => {
4974
setSeeKeywordsModal(true);
@@ -59,39 +84,54 @@ export function JobsFiltersCard({
5984
return;
6085
}
6186

62-
setKeywordFilter((current) => (current.includes(value) ? current : [...current, value]));
63-
};
64-
65-
const handleRemoveFilter = (filterToRemove: string) => {
66-
setKeywordFilter((current) => current.filter((item) => item !== filterToRemove));
67-
setSearch((current) =>
68-
current
69-
.split(/[,;/]+/)
70-
.map((item) => item.trim())
71-
.filter((item) => item && item !== filterToRemove)
72-
.join(", "),
87+
setKeywordFilter((current) =>
88+
current.includes(value) ? current : [...current, value],
7389
);
7490
};
7591

76-
const handleClearFilters = () => {
77-
setSearch("");
78-
setKeywordFilter([]);
92+
const handleAddSearchTerm = (event: FormEvent<HTMLFormElement>) => {
93+
event.preventDefault();
94+
95+
const nextTerm = searchTerm.trim();
96+
97+
if (!nextTerm) {
98+
return;
99+
}
100+
101+
setSearch((current) => {
102+
const currentTerms = getSelectedFilters(current, []);
103+
104+
if (currentTerms.includes(nextTerm)) {
105+
return current;
106+
}
107+
108+
return [...currentTerms, nextTerm].join(", ");
109+
});
110+
setSearchTerm("");
79111
};
80112

81113
return (
82114
<>
83115
<Card className="rounded-[24px] border border-slate-200 bg-white/95 shadow-[0_18px_45px_rgba(15,23,42,0.08)] backdrop-blur dark:border-[#2b3f58] dark:bg-[#131d31]/95 dark:shadow-[0_18px_45px_rgba(3,8,20,0.25)]">
84116
<CardContent className="flex flex-col gap-4 p-5 md:p-6">
85117
<div className="flex flex-col md:flex-row gap-3 md:items-center">
86-
<div className="relative flex-1">
118+
<form className="relative flex-1" onSubmit={handleAddSearchTerm}>
87119
<FiSearch className="pointer-events-none absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-500 dark:text-slate-400" />
88120
<Input
89-
value={search}
90-
onChange={(event) => setSearch(event.target.value)}
91-
className="h-14 w-full rounded-2xl border border-slate-300 bg-white pl-11 text-base text-slate-900 placeholder:text-slate-500 focus-visible:ring-[#14AE5C]/40 dark:border-[#35506f] dark:bg-[#091224] dark:text-slate-100 dark:placeholder:text-slate-400"
121+
value={searchTerm}
122+
onChange={(event) => setSearchTerm(event.target.value)}
123+
className="h-14 w-full rounded-2xl border border-slate-300 bg-white pl-11 pr-14 text-base text-slate-900 placeholder:text-slate-500 focus-visible:ring-[#14AE5C]/40 dark:border-[#35506f] dark:bg-[#091224] dark:text-slate-100 dark:placeholder:text-slate-400"
92124
placeholder="Buscar por título, empresa, local ou link"
93125
/>
94-
</div>
126+
<button
127+
type="submit"
128+
aria-label="Adicionar filtro de busca"
129+
className="absolute right-2 top-1/2 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-xl bg-[#0c6b35] text-white transition-colors hover:bg-[#0a5b2d] disabled:cursor-not-allowed disabled:opacity-50"
130+
disabled={!searchTerm.trim()}
131+
>
132+
<FiPlus className="h-4 w-4" />
133+
</button>
134+
</form>
95135
<div className="flex items-center gap-2">{actions}</div>
96136
</div>
97137

@@ -124,15 +164,18 @@ export function JobsFiltersCard({
124164
))}
125165
</select>
126166

127-
<div>
128-
<Badge variant="secondary" className="gap-1.5 rounded-full border border-slate-300 bg-slate-100 px-3 py-1 text-xs text-slate-700 dark:border-[#35506f] dark:bg-[#24324c] dark:text-slate-100">
167+
<div>
168+
<Badge
169+
variant="secondary"
170+
className="gap-1.5 rounded-full border border-slate-300 bg-slate-100 px-3 py-1 text-xs text-slate-700 dark:border-[#35506f] dark:bg-[#24324c] dark:text-slate-100"
171+
>
129172
<FiFileText className="h-3.5 w-3.5" />
130-
{meta.file || "Sem arquivo"}
173+
{meta.file || "Sem arquivo"}
131174
</Badge>
132175

133176
<Badge className="gap-1.5 rounded-full bg-[#0c6b35] px-3 py-1 text-xs text-white">
134177
<FiBriefcase className="h-3.5 w-3.5" />
135-
{meta.total} vagas
178+
{meta.total} vagas
136179
</Badge>
137180
</div>
138181
</div>
@@ -151,7 +194,7 @@ export function JobsFiltersCard({
151194
variant="outline"
152195
aria-label="Limpar filtros"
153196
className="h-10 w-10 rounded-md border-[#0c6b35] bg-[#0c6b35] p-0 text-white shadow-sm hover:bg-[#0a5b2d]"
154-
onClick={handleClearFilters}
197+
onClick={onClearFilters}
155198
>
156199
<FiSlash className="h-4 w-4" />
157200
</Button>
@@ -160,7 +203,9 @@ export function JobsFiltersCard({
160203

161204
<div className="rounded-2xl border border-slate-200 bg-slate-50 px-3 py-3 dark:border-[#2b3f58] dark:bg-[#12203a]/45">
162205
<div className="flex flex-wrap items-center gap-2">
163-
<span className="text-[11px] font-medium uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400">Filtros selecionados</span>
206+
<span className="text-[11px] font-medium uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400">
207+
Filtros selecionados
208+
</span>
164209
{selectedFilters.length > 0 ? (
165210
selectedFilters.map((filter) => (
166211
<Badge
@@ -174,20 +219,24 @@ export function JobsFiltersCard({
174219
type="button"
175220
aria-label={`Remover filtro ${filter}`}
176221
className="ml-1 rounded-full p-0.5 text-[#0c6b35] hover:bg-[#14AE5C]/20 dark:text-[#8df0af]"
177-
onClick={() => handleRemoveFilter(filter)}
222+
onClick={() => onRemoveFilter(filter)}
178223
>
179224
<FiSlash className="h-3 w-3" />
180225
</button>
181226
</Badge>
182227
))
183228
) : (
184-
<span className="text-sm text-slate-500 dark:text-slate-400">Use o botão Filtrar para adicionar ou remover palavras-chave.</span>
229+
<span className="text-sm text-slate-500 dark:text-slate-400">
230+
Use o botão Filtrar para adicionar ou remover palavras-chave.
231+
</span>
185232
)}
186233
</div>
187234
</div>
188235
</CardContent>
189236
</Card>
190-
{seeKeywordsModal && <KeywordsModal onClose={() => setSeeKeywordsModal(false)} />}
237+
{seeKeywordsModal && (
238+
<KeywordsModal onClose={() => setSeeKeywordsModal(false)} />
239+
)}
191240
</>
192241
);
193242
}

0 commit comments

Comments
 (0)