← back to Open Seo

src/client/features/keywords/hooks/useKeywordFiltering.test.ts

108 lines

import { describe, expect, it } from "vitest";
import type { KeywordIntent, KeywordResearchRow } from "@/types/keywords";
import {
  EMPTY_FILTERS,
  parseIntentFilter,
  toggleIntentFilter,
  type KeywordFilterValues,
} from "@/client/features/keywords/keywordResearchTypes";
import { applyKeywordFiltersAndSort } from "./useKeywordFiltering";

function makeRow(keyword: string, intent: KeywordIntent): KeywordResearchRow {
  return {
    keyword,
    searchVolume: 100,
    trend: [],
    keywordDifficulty: 10,
    cpc: 1,
    competition: 0.5,
    intent,
  };
}

function filter(
  rows: KeywordResearchRow[],
  overrides: Partial<KeywordFilterValues>,
): KeywordResearchRow[] {
  return applyKeywordFiltersAndSort({
    rows,
    filters: { ...EMPTY_FILTERS, ...overrides },
    sortField: "keyword",
    sortDir: "asc",
  });
}

const rows: KeywordResearchRow[] = [
  makeRow("buy running shoes", "transactional"),
  makeRow("best running shoes", "commercial"),
  makeRow("how to run", "informational"),
  makeRow("nike store", "navigational"),
  makeRow("mystery term", "unknown"),
];

describe("parseIntentFilter", () => {
  it("returns an empty list for an empty string", () => {
    expect(parseIntentFilter("")).toEqual([]);
  });

  it("parses a comma-separated string in canonical order", () => {
    expect(parseIntentFilter("transactional,informational")).toEqual([
      "informational",
      "transactional",
    ]);
  });

  it("drops unknown tokens and de-duplicates", () => {
    expect(parseIntentFilter("commercial,bogus,commercial")).toEqual([
      "commercial",
    ]);
  });
});

describe("toggleIntentFilter", () => {
  it("adds an intent when absent and keeps canonical order", () => {
    expect(toggleIntentFilter("transactional", "informational")).toBe(
      "informational,transactional",
    );
  });

  it("removes an intent when already present", () => {
    expect(
      toggleIntentFilter("informational,transactional", "informational"),
    ).toBe("transactional");
  });
});

describe("applyKeywordFiltersAndSort — intent filtering", () => {
  it("returns every row when no intent is selected", () => {
    expect(filter(rows, { intents: "" })).toHaveLength(rows.length);
  });

  it("keeps only rows matching a single selected intent", () => {
    const result = filter(rows, { intents: "transactional" });
    expect(result.map((r) => r.keyword)).toEqual(["buy running shoes"]);
  });

  it("keeps rows matching any of multiple selected intents", () => {
    const result = filter(rows, { intents: "transactional,commercial" });
    expect(result.map((r) => r.keyword).toSorted()).toEqual([
      "best running shoes",
      "buy running shoes",
    ]);
  });

  it("combines the intent filter with other filters (AND)", () => {
    // "running" narrows to the two shoe rows; intent narrows to the commercial one.
    const result = filter(rows, {
      include: "running",
      intents: "commercial",
    });
    expect(result.map((r) => r.keyword)).toEqual(["best running shoes"]);
  });

  it("ignores invalid intent tokens (treated as no intent match constraint)", () => {
    const result = filter(rows, { intents: "bogus" });
    expect(result).toHaveLength(rows.length);
  });
});