Refactor search.js (#8007)
Refactored buyersguide search.js, including: - PNI tests for search - github workflow update to run integration testing on CI
This commit is contained in:
Родитель
b5e42ed1ed
Коммит
43a9954dc9
|
@ -156,3 +156,63 @@ jobs:
|
|||
run: npm run percy
|
||||
env:
|
||||
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
||||
|
||||
test_integration:
|
||||
name: Integration testing
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13.2
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: network
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
env:
|
||||
ALLOWED_HOSTS: localhost,mozfest.localhost,default-site.com,secondary-site.com
|
||||
CONTENT_TYPE_NO_SNIFF: True
|
||||
CORS_ALLOWED_ORIGINS: "*"
|
||||
DATABASE_URL: postgres://postgres:postgres@localhost:5432/network
|
||||
DEBUG: True
|
||||
DJANGO_SECRET_KEY: secret
|
||||
DOMAIN_REDIRECT_MIDDLEWARE_ENABLED: False
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NETWORK_SITE_URL: https://foundation.mozilla.org
|
||||
PIPENV_VERBOSITY: -1
|
||||
PULSE_API_DOMAIN: https://network-pulse-api-production.herokuapp.com
|
||||
PULSE_DOMAIN: https://www.mozillapulse.org
|
||||
RANDOM_SEED: 530910203
|
||||
SET_HSTS: False
|
||||
SSL_REDIRECT: False
|
||||
TARGET_DOMAINS: foundation.mozilla.org
|
||||
USE_S3: False
|
||||
X_FRAME_OPTIONS: DENY
|
||||
XSS_PROTECTION: True
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9.9
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Install Python Dependencies
|
||||
run: pip install -r requirements.txt -r dev-requirements.txt
|
||||
- name: Install Node Dependencies
|
||||
run: npm ci
|
||||
- name: Install additional tooling
|
||||
run: |
|
||||
sudo apt-get install -y gettext libgconf-2-4
|
||||
- name: Install Playwright
|
||||
run: npm run playwright:install
|
||||
- name: Preroll
|
||||
run: |
|
||||
npm run build
|
||||
python network-api/manage.py collectstatic --no-input --verbosity 0
|
||||
python network-api/manage.py migrate --no-input
|
||||
python network-api/manage.py block_inventory
|
||||
python network-api/manage.py load_fake_data
|
||||
- name: Integration Tests
|
||||
run: npm run playwright:ci
|
||||
|
|
|
@ -46,6 +46,7 @@ const sources = {
|
|||
},
|
||||
"bg-search": {
|
||||
source: `buyers-guide/search.js`,
|
||||
bundle: true,
|
||||
},
|
||||
polyfills: {
|
||||
source: `polyfills.js`,
|
||||
|
|
|
@ -112,6 +112,11 @@ class ProductPageFactory(PageFactory):
|
|||
product=self,
|
||||
category=category
|
||||
)
|
||||
if category.parent:
|
||||
ProductPageCategory.objects.get_or_create(
|
||||
product=self,
|
||||
category=category.parent
|
||||
)
|
||||
ceiling = ceiling / 5
|
||||
else:
|
||||
return
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
from django.db import migrations
|
||||
from django.template.defaultfilters import slugify
|
||||
|
||||
def add_pni_subcategories(apps, schema):
|
||||
BuyersGuideProductCategory = apps.get_model("wagtailpages", "BuyersGuideProductCategory")
|
||||
healthAndExercise = BuyersGuideProductCategory.objects.get(name="Health & Exercise")
|
||||
|
||||
subcategories = [
|
||||
"Exercise Equipment",
|
||||
"Smart Scales",
|
||||
"Smart Thermometers",
|
||||
]
|
||||
|
||||
for cat in subcategories:
|
||||
print(f"creating {cat}")
|
||||
subcategory, created = BuyersGuideProductCategory.objects.get_or_create(
|
||||
name=cat,
|
||||
parent=healthAndExercise,
|
||||
locale_id=healthAndExercise.locale_id,
|
||||
)
|
||||
if created:
|
||||
subcategory.slug = slugify(cat)
|
||||
subcategory.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtailpages', '0063_pulsefilter_pulsefilteroption'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
add_pni_subcategories
|
||||
),
|
||||
]
|
|
@ -9745,23 +9745,6 @@
|
|||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.17.0.tgz",
|
||||
"integrity": "sha512-rBBrdj5+iYFBSsMa7glK3MBaxkcXxHN7uW6pY7PzTB7xtGFTfdK0h0BPB7/vWiVIGOvz4Hy9g2UJNaZDHkAmgA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "=1.17.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.17.0.tgz",
|
||||
|
@ -22477,16 +22460,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"playwright": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.17.0.tgz",
|
||||
"integrity": "sha512-rBBrdj5+iYFBSsMa7glK3MBaxkcXxHN7uW6pY7PzTB7xtGFTfdK0h0BPB7/vWiVIGOvz4Hy9g2UJNaZDHkAmgA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"playwright-core": "=1.17.0"
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.17.0.tgz",
|
||||
|
|
|
@ -32,14 +32,15 @@
|
|||
"optimize:jpg": "find source/images -type f -name '*.jpg' -print0 | xargs -0 -n 1 -P 6 -I '{}' guetzli --quality 93 '{}' '{}'",
|
||||
"optimize:png": "find source/images -type f -name '*.png' -print0 | xargs -0 -n 1 -P 6 optipng",
|
||||
"optimize": "run-p optimize:**",
|
||||
"percy": "percy exec -t 750 -- npm run playwright:ci",
|
||||
"percy": "percy exec -t 750 -- npm run playwright:percy",
|
||||
"playwright": "npm run playwright:wait:dev && playwright test ./tests/integration.spec.js",
|
||||
"playwright:install": "playwright install chromium firefox webkit",
|
||||
"playwright:percy": "npm run playwright:wait:dev && playwright test ./tests/visual.spec.js",
|
||||
"playwright:wait:dev": "wait-on -i 3000 http://localhost:8000/static/_css/tailwind.compiled.css",
|
||||
"playwright:ci": "run-p --race server playwright:visual",
|
||||
"playwright:percy": "run-p --race server playwright:visual",
|
||||
"playwright:urls": "wait-on -i 3000 http://localhost:8000/cms && playwright test ./tests/urls.spec.js",
|
||||
"playwright:visual": "wait-on -i 3000 http://localhost:8000/cms && playwright test ./tests/visual.spec.js",
|
||||
"playwright:ci": "run-p --race server playwright:integration",
|
||||
"playwright:integration": "wait-on -i 3000 http://localhost:8000/cms && playwright test ./tests/integration.spec.js",
|
||||
"precommit": "npm run test",
|
||||
"server": "python network-api/manage.py runserver 0.0.0.0:8000",
|
||||
"server:silent": "python network-api/manage.py runserver 0.0.0.0:8000 >> server.log 2>&1",
|
||||
|
|
|
@ -1,797 +1,9 @@
|
|||
/**
|
||||
* Set up PNI search functionality, as well as the PNI "ding" toggle.
|
||||
* Set up the search/filter functionality for PNI pages
|
||||
*/
|
||||
|
||||
// static values used throughout this code
|
||||
const ALL_PRODUCTS = document.querySelectorAll(`figure.product-box`);
|
||||
const NO_RESULTS_NOTICE = document.getElementById(
|
||||
`product-filter-no-results-notice`
|
||||
);
|
||||
const FILTERS = [`company`, `name`, `blurb`, `worst-case`];
|
||||
const SORTS = [`name`, `company`, `blurb`];
|
||||
const SUBMIT_PRODUCT = document.querySelector(".recommend-product");
|
||||
const CREEPINESS_FACE = document.querySelector(".creep-o-meter-information");
|
||||
const categoryTitle = document.querySelector(`.category-title`);
|
||||
const parentTitle = document.querySelector(`.parent-title`);
|
||||
const toggle = document.querySelector(`#product-filter-pni-toggle`);
|
||||
const subcategories = document.querySelectorAll(`.subcategories`);
|
||||
const subContainer = document.querySelector(`.subcategory-header`);
|
||||
import { SearchFilter } from "./search/search-filter.js";
|
||||
import { PNIToggle } from "./search/pni-toggle.js";
|
||||
|
||||
// TODO: turn this into a static class rather than plain JS object.
|
||||
const SearchFilter = {
|
||||
init: () => {
|
||||
let pos = { left: 0, x: 0 };
|
||||
const subClasses = subContainer.classList;
|
||||
|
||||
const markScrollStart = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
subClasses.add("cursor-grabbing", "select-none");
|
||||
|
||||
pos = {
|
||||
left: subContainer.scrollLeft,
|
||||
x: event.clientX,
|
||||
};
|
||||
|
||||
[`mousemove`, `touchmove`].forEach((type) =>
|
||||
document.addEventListener(type, markScrollMove)
|
||||
);
|
||||
|
||||
[`mouseup`, `touchend`, `touchcancel`].forEach((type) =>
|
||||
document.addEventListener(type, markScrollEnd)
|
||||
);
|
||||
};
|
||||
|
||||
const markScrollMove = (event) => {
|
||||
subcategories.forEach((subcategory) => {
|
||||
subcategory.classList.add("pointer-events-none");
|
||||
});
|
||||
const dx = event.clientX - pos.x;
|
||||
subContainer.scrollLeft = pos.left - dx;
|
||||
};
|
||||
|
||||
const markScrollEnd = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
subcategories.forEach((subcategory) => {
|
||||
subcategory.classList.remove("pointer-events-none");
|
||||
});
|
||||
|
||||
subClasses.remove("cursor-grabbing", "select-none");
|
||||
|
||||
[`mousemove`, `touchmove`].forEach((type) =>
|
||||
document.removeEventListener(type, markScrollMove)
|
||||
);
|
||||
|
||||
[`mouseup`, `touchend`, `touchcancel`].forEach((type) =>
|
||||
document.removeEventListener(type, markScrollEnd)
|
||||
);
|
||||
};
|
||||
|
||||
[`mousedown`, `touchstart`].forEach((type) =>
|
||||
subContainer.addEventListener(type, markScrollStart)
|
||||
);
|
||||
|
||||
const searchBar = document.querySelector(`#product-filter-search`);
|
||||
|
||||
if (!searchBar) {
|
||||
return console.warn(
|
||||
`Could not find the PNI search bar. Search will not be available.`
|
||||
);
|
||||
}
|
||||
|
||||
const searchInput = (SearchFilter.searchInput =
|
||||
searchBar.querySelector(`input`));
|
||||
|
||||
searchInput.addEventListener(`input`, (evt) => {
|
||||
const searchText = searchInput.value.trim();
|
||||
|
||||
if (searchText) {
|
||||
searchBar.classList.add(`has-content`);
|
||||
SearchFilter.filter(searchText);
|
||||
} else {
|
||||
clearText();
|
||||
applyHistory();
|
||||
}
|
||||
});
|
||||
|
||||
const clear = searchBar.querySelector(`.clear-icon`);
|
||||
if (!clear) {
|
||||
return console.warn(
|
||||
`Could not find the PNI search input clear icon. Search will work, but clearing will not.`
|
||||
);
|
||||
}
|
||||
|
||||
const clearText = () => {
|
||||
searchBar.classList.remove(`has-content`);
|
||||
searchInput.value = ``;
|
||||
ALL_PRODUCTS.forEach((product) => {
|
||||
product.classList.remove(`d-none`);
|
||||
product.classList.add(`d-flex`);
|
||||
});
|
||||
|
||||
history.replaceState(
|
||||
{
|
||||
...history.state,
|
||||
search: "",
|
||||
},
|
||||
SearchFilter.getTitle(categoryTitle.value.trim()),
|
||||
location.href
|
||||
);
|
||||
|
||||
SearchFilter.sortOnCreepiness();
|
||||
SearchFilter.moveCreepyFace();
|
||||
};
|
||||
|
||||
const applyHistory = () => {
|
||||
const { category, parent } = history.state;
|
||||
|
||||
categoryTitle.value = category;
|
||||
parentTitle.value = parent;
|
||||
|
||||
if (parent) {
|
||||
SearchFilter.highlightParent();
|
||||
SearchFilter.toggleSubcategory();
|
||||
} else {
|
||||
document
|
||||
.querySelector(`#multipage-nav a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#multipage-nav a[data-name="${category}"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a[data-name="${category}"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
SearchFilter.toggleSubcategory(true);
|
||||
}
|
||||
SearchFilter.filterCategory(category);
|
||||
SearchFilter.filterSubcategory(parent || category);
|
||||
SearchFilter.updateHeader(category, parent);
|
||||
SearchFilter.sortOnCreepiness();
|
||||
SearchFilter.moveCreepyFace();
|
||||
|
||||
if (history.state?.parent && history.state?.category) {
|
||||
document
|
||||
.querySelector(
|
||||
`a.subcategories[data-name="${history.state?.category}"]`
|
||||
)
|
||||
.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "start",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
clear.addEventListener(`click`, (evt) => {
|
||||
evt.preventDefault();
|
||||
searchInput.focus();
|
||||
clearText();
|
||||
applyHistory();
|
||||
});
|
||||
|
||||
const navLinks = document.querySelectorAll(
|
||||
`#multipage-nav a,.category-header,#pni-nav-mobile a`
|
||||
);
|
||||
|
||||
for (const nav of navLinks) {
|
||||
nav.addEventListener("click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
|
||||
if (evt.shiftKey || evt.metaKey || evt.ctrlKey || evt.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
document
|
||||
.querySelector(`#multipage-nav a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector("#pni-nav-mobile .dropdown-nav")
|
||||
.classList.remove("dropdown-nav-open");
|
||||
|
||||
if (evt.target.dataset.name) {
|
||||
document
|
||||
.querySelector(
|
||||
`#multipage-nav a[data-name="${evt.target.dataset.name}"]`
|
||||
)
|
||||
.classList.add(`active`);
|
||||
|
||||
document
|
||||
.querySelector(
|
||||
`#pni-nav-mobile a[data-name="${evt.target.dataset.name}"]`
|
||||
)
|
||||
.classList.add(`active`);
|
||||
|
||||
clearText();
|
||||
history.pushState(
|
||||
{
|
||||
title: SearchFilter.getTitle(evt.target.dataset.name),
|
||||
category: evt.target.dataset.name,
|
||||
parent: "",
|
||||
search: "",
|
||||
filter: history.state?.filter,
|
||||
},
|
||||
SearchFilter.getTitle(evt.target.dataset.name),
|
||||
evt.target.href
|
||||
);
|
||||
|
||||
document.title = SearchFilter.getTitle(evt.target.dataset.name);
|
||||
SearchFilter.filterSubcategory(evt.target.dataset.name);
|
||||
SearchFilter.toggleSubcategory(true);
|
||||
SearchFilter.updateHeader(evt.target.dataset.name, "");
|
||||
SearchFilter.filterCategory(evt.target.dataset.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const subcategory of subcategories) {
|
||||
subcategory.addEventListener(
|
||||
"click",
|
||||
(evt) => {
|
||||
evt.stopImmediatePropagation();
|
||||
if (evt.shiftKey || evt.metaKey || evt.ctrlKey || evt.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
let href;
|
||||
|
||||
if (evt.target.dataset.name) {
|
||||
clearText();
|
||||
if (categoryTitle.value.trim() !== evt.target.dataset.name) {
|
||||
categoryTitle.value = evt.target.dataset.name;
|
||||
parentTitle.value = evt.target.dataset.parent;
|
||||
href = evt.target.href;
|
||||
SearchFilter.toggleSubcategory();
|
||||
SearchFilter.highlightParent();
|
||||
} else {
|
||||
categoryTitle.value = evt.target.dataset.parent;
|
||||
parentTitle.value = "";
|
||||
href = document.querySelector(
|
||||
`#multipage-nav a[data-name="${evt.target.dataset.parent}"]`
|
||||
).href;
|
||||
SearchFilter.toggleSubcategory(true);
|
||||
}
|
||||
|
||||
history.pushState(
|
||||
{
|
||||
title: SearchFilter.getTitle(evt.target.dataset.name),
|
||||
category: categoryTitle.value.trim(),
|
||||
parent: parentTitle.value.trim(),
|
||||
search: "",
|
||||
filter: history.state?.filter,
|
||||
},
|
||||
SearchFilter.getTitle(evt.target.dataset.name),
|
||||
href
|
||||
);
|
||||
|
||||
document.title = SearchFilter.getTitle(categoryTitle.value.trim());
|
||||
SearchFilter.updateHeader(
|
||||
categoryTitle.value.trim(),
|
||||
parentTitle.value.trim()
|
||||
);
|
||||
SearchFilter.filterCategory(categoryTitle.value.trim());
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector(`.go-back-to-all-link`)
|
||||
.addEventListener("click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
|
||||
if (evt.shiftKey || evt.metaKey || evt.ctrlKey || evt.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
clearText();
|
||||
history.pushState(
|
||||
{
|
||||
title: SearchFilter.getTitle("None"),
|
||||
category: "None",
|
||||
parent: "",
|
||||
search: "",
|
||||
filter: history.state?.filter,
|
||||
},
|
||||
SearchFilter.getTitle(evt.target.dataset.name),
|
||||
evt.target.href
|
||||
);
|
||||
|
||||
document
|
||||
.querySelector(`#multipage-nav a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#multipage-nav a[data-name="None"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a[data-name="None"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
SearchFilter.filterCategory("None");
|
||||
parentTitle.value = "";
|
||||
});
|
||||
|
||||
window.addEventListener(`popstate`, (event) => {
|
||||
const { state } = event;
|
||||
if (!state) return; // if it's a "real" back, we shouldn't need to do anything
|
||||
|
||||
const { title, category, parent } = state;
|
||||
document.title = title;
|
||||
|
||||
if (!history.state?.search) {
|
||||
SearchFilter.clearCategories();
|
||||
categoryTitle.value = category;
|
||||
parentTitle.value = parent;
|
||||
|
||||
searchBar.classList.remove(`has-content`);
|
||||
searchInput.value = ``;
|
||||
|
||||
if (parent) {
|
||||
SearchFilter.highlightParent();
|
||||
SearchFilter.toggleSubcategory();
|
||||
} else {
|
||||
document
|
||||
.querySelector(`#multipage-nav a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#multipage-nav a[data-name="${category}"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a[data-name="${category}"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
SearchFilter.toggleSubcategory(true);
|
||||
}
|
||||
} else {
|
||||
SearchFilter.toggleSubcategory(true);
|
||||
searchBar.classList.add(`has-content`);
|
||||
searchInput.value = history.state?.search;
|
||||
SearchFilter.filter(history.state?.search);
|
||||
}
|
||||
|
||||
SearchFilter.filterCategory(category);
|
||||
SearchFilter.filterSubcategory(parent || category);
|
||||
SearchFilter.updateHeader(category, parent);
|
||||
|
||||
if (history.state?.filter) {
|
||||
toggle.checked = history.state?.filter;
|
||||
|
||||
if (history.state?.filter) {
|
||||
document.body.classList.add(`show-ding-only`);
|
||||
} else {
|
||||
document.body.classList.remove(`show-ding-only`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
history.replaceState(
|
||||
{
|
||||
title: SearchFilter.getTitle(categoryTitle.value.trim()),
|
||||
category: categoryTitle.value.trim(),
|
||||
parent: parentTitle.value.trim(),
|
||||
search: history.state?.search ?? "",
|
||||
filter: history.state?.filter,
|
||||
},
|
||||
SearchFilter.getTitle(categoryTitle.value.trim()),
|
||||
location.href
|
||||
);
|
||||
|
||||
if (history.state?.search) {
|
||||
searchBar.classList.add(`has-content`);
|
||||
searchInput.value = history.state?.search;
|
||||
SearchFilter.filter(history.state?.search);
|
||||
} else {
|
||||
searchBar.classList.remove(`has-content`);
|
||||
searchInput.value = ``;
|
||||
}
|
||||
|
||||
if (history.state?.filter) {
|
||||
toggle.checked = history.state?.filter;
|
||||
|
||||
if (history.state?.filter) {
|
||||
document.body.classList.add(`show-ding-only`);
|
||||
} else {
|
||||
document.body.classList.remove(`show-ding-only`);
|
||||
}
|
||||
}
|
||||
|
||||
if (history.state?.parent && history.state?.category) {
|
||||
document
|
||||
.querySelector(
|
||||
`a.subcategories[data-name="${history.state?.category}"]`
|
||||
)
|
||||
.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "start",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
clearCategories: () => {
|
||||
SearchFilter.filterCategory("None");
|
||||
parentTitle.value = null;
|
||||
|
||||
if (document.querySelector(`#multipage-nav a.active`)) {
|
||||
document
|
||||
.querySelector(`#multipage-nav a.active`)
|
||||
.classList.remove(`active`);
|
||||
document
|
||||
.querySelector(`#multipage-nav a[data-name="None"]`)
|
||||
.classList.add(`active`);
|
||||
}
|
||||
|
||||
if (document.querySelector(`#pni-nav-mobile a.active`)) {
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a.active`)
|
||||
.classList.remove(`active`);
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a[data-name="None"]`)
|
||||
.classList.add(`active`);
|
||||
}
|
||||
},
|
||||
|
||||
updateHeader: (category, parent) => {
|
||||
if (parent) {
|
||||
document.querySelector(".category-header").textContent = parent;
|
||||
document.querySelector(".category-header").dataset.name = parent;
|
||||
document.querySelector(".category-header").href = document.querySelector(
|
||||
`#multipage-nav a[data-name="${parent}"]`
|
||||
).href;
|
||||
document.querySelector(`#pni-nav-mobile .active-link-label`).textContent =
|
||||
parent;
|
||||
} else {
|
||||
const header = category === "None" ? gettext("All") : category;
|
||||
document.querySelector(".category-header").textContent = header;
|
||||
document.querySelector(".category-header").dataset.name = category;
|
||||
document.querySelector(".category-header").href = document.querySelector(
|
||||
`#multipage-nav a[data-name="${category}"]`
|
||||
).href;
|
||||
document.querySelector(`#pni-nav-mobile .active-link-label`).textContent =
|
||||
category === "None"
|
||||
? document.querySelector(`#multipage-nav a[data-name="None"]`)
|
||||
.textContent
|
||||
: category;
|
||||
}
|
||||
},
|
||||
|
||||
filterSubcategory: (category) => {
|
||||
for (const subcategory of subcategories) {
|
||||
if (subcategory.dataset.parent === category) {
|
||||
subcategory.classList.remove(`tw-hidden`);
|
||||
} else {
|
||||
subcategory.classList.add(`tw-hidden`);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getTitle: (category) => {
|
||||
if (category == "None")
|
||||
return document.querySelector('meta[name="pni-home-title"]').content;
|
||||
else {
|
||||
return `${category} | ${
|
||||
document.querySelector('meta[name="pni-category-title"]').content
|
||||
}`;
|
||||
}
|
||||
},
|
||||
|
||||
moveCreepyFace: () => {
|
||||
// When searching, check to see how many products are still visible
|
||||
// If there are no visible products, there are "no search results"
|
||||
// And when there are no search results, do not show the creepo-meter-face
|
||||
if (document.querySelectorAll(".product-box:not(.d-none)").length) {
|
||||
// If there are search results, show the creepo-meter-face
|
||||
CREEPINESS_FACE.classList.remove("d-none");
|
||||
} else {
|
||||
// If there are no search results, hide the creepo-meter-face
|
||||
CREEPINESS_FACE.classList.add("d-none");
|
||||
}
|
||||
},
|
||||
|
||||
filter: (text) => {
|
||||
// remove category filters
|
||||
SearchFilter.clearCategories();
|
||||
SearchFilter.toggleSubcategory(true);
|
||||
SearchFilter.filterSubcategory("None");
|
||||
SearchFilter.updateHeader("None", null);
|
||||
|
||||
if (document.querySelector(`#multipage-nav a.active`)) {
|
||||
document
|
||||
.querySelector(`#multipage-nav a.active`)
|
||||
.classList.remove(`active`);
|
||||
}
|
||||
|
||||
if (document.querySelector(`#pni-nav-mobile a.active`)) {
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a.active`)
|
||||
.classList.remove(`active`);
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector(`#multipage-nav a[data-name="None"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a[data-name="None"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
ALL_PRODUCTS.forEach((product) => {
|
||||
if (SearchFilter.test(product, text)) {
|
||||
product.classList.remove(`d-none`);
|
||||
product.classList.add(`d-flex`);
|
||||
} else {
|
||||
product.classList.add(`d-none`);
|
||||
product.classList.remove(`d-flex`);
|
||||
}
|
||||
});
|
||||
|
||||
history.replaceState(
|
||||
{
|
||||
...history.state,
|
||||
search: text,
|
||||
},
|
||||
SearchFilter.getTitle(categoryTitle.value.trim()),
|
||||
location.href
|
||||
);
|
||||
|
||||
SearchFilter.sortProducts();
|
||||
|
||||
SearchFilter.moveCreepyFace();
|
||||
SearchFilter.checkForEmptyNotice();
|
||||
},
|
||||
|
||||
sortProducts: () => {
|
||||
const container = document.querySelector(`.product-box-list`);
|
||||
const list = [...container.querySelectorAll(`.product-box`)];
|
||||
|
||||
list.sort((a, b) => {
|
||||
for (const field of SORTS) {
|
||||
const qs = `.product-${field}`;
|
||||
const [propertyA, propertyB] = [
|
||||
a.querySelector(qs),
|
||||
b.querySelector(qs),
|
||||
];
|
||||
const [propertyNameA, propertyNameB] = [
|
||||
(propertyA.value || propertyA.textContent).toLowerCase(),
|
||||
(propertyB.value || propertyB.textContent).toLowerCase(),
|
||||
];
|
||||
|
||||
if (
|
||||
propertyNameA !== propertyNameB ||
|
||||
field === SORTS[SORTS.length - 1]
|
||||
) {
|
||||
return propertyNameA < propertyNameB
|
||||
? -1
|
||||
: propertyNameA > propertyNameB
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
list.forEach((p) => container.append(p));
|
||||
},
|
||||
|
||||
sortOnCreepiness: () => {
|
||||
const container = document.querySelector(`.product-box-list`);
|
||||
const list = [...container.querySelectorAll(`.product-box`)];
|
||||
const creepVal = (e) => parseFloat(e.dataset.creepiness);
|
||||
list
|
||||
.sort((a, b) => creepVal(a) - creepVal(b))
|
||||
.forEach((p) => container.append(p));
|
||||
},
|
||||
|
||||
filterCategory: (category) => {
|
||||
ALL_PRODUCTS.forEach((product) => {
|
||||
if (SearchFilter.testCategories(product, category)) {
|
||||
product.classList.remove(`d-none`);
|
||||
product.classList.add(`d-flex`);
|
||||
} else {
|
||||
product.classList.add(`d-none`);
|
||||
product.classList.remove(`d-flex`);
|
||||
}
|
||||
});
|
||||
|
||||
categoryTitle.value = category;
|
||||
SearchFilter.sortOnCreepiness();
|
||||
SearchFilter.moveCreepyFace();
|
||||
SearchFilter.checkForEmptyNotice();
|
||||
},
|
||||
|
||||
highlightParent: () => {
|
||||
if (document.querySelector(`#multipage-nav a.active`)) {
|
||||
document
|
||||
.querySelector(`#multipage-nav a.active`)
|
||||
.classList.remove(`active`);
|
||||
}
|
||||
|
||||
if (document.querySelector(`#pni-nav-mobile a.active`)) {
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a.active`)
|
||||
.classList.remove(`active`);
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector(
|
||||
`#pni-nav-mobile a[data-name="${parentTitle.value.trim()}"]`
|
||||
)
|
||||
.classList.add(`active`);
|
||||
|
||||
document
|
||||
.querySelector(
|
||||
`#multipage-nav a[data-name="${parentTitle.value.trim()}"]`
|
||||
)
|
||||
.classList.add(`active`);
|
||||
},
|
||||
|
||||
toggleSubcategory: (clear = false) => {
|
||||
const activeClasses = [
|
||||
"active",
|
||||
"tw-bg-gray-80",
|
||||
"tw-text-white",
|
||||
"tw-border-gray-80",
|
||||
];
|
||||
const defaultClasses = [
|
||||
"hover:tw-border-pni-lilac",
|
||||
"hover:tw-bg-pni-lilac",
|
||||
"tw-text-gray-60",
|
||||
"tw-border-gray-20",
|
||||
"tw-bg-white",
|
||||
];
|
||||
|
||||
if (document.querySelector(`a.subcategories.active`)) {
|
||||
document
|
||||
.querySelector(`a.subcategories.active`)
|
||||
.classList.add(...defaultClasses);
|
||||
document
|
||||
.querySelector(`a.subcategories.active`)
|
||||
.classList.remove(...activeClasses);
|
||||
}
|
||||
|
||||
if (clear) {
|
||||
return;
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector(
|
||||
`a.subcategories[data-name="${categoryTitle.value.trim()}"]`
|
||||
)
|
||||
.classList.add(...activeClasses);
|
||||
|
||||
document
|
||||
.querySelector(
|
||||
`a.subcategories[data-name="${categoryTitle.value.trim()}"]`
|
||||
)
|
||||
.classList.remove(...defaultClasses);
|
||||
|
||||
document
|
||||
.querySelector(
|
||||
`a.subcategories[data-name="${categoryTitle.value.trim()}"]`
|
||||
)
|
||||
.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "start",
|
||||
});
|
||||
},
|
||||
|
||||
checkForEmptyNotice: () => {
|
||||
let qs = `figure.product-box:not(.d-none)`;
|
||||
|
||||
if (document.body.classList.contains(`show-ding-only`)) {
|
||||
qs = `${qs}.privacy-ding`;
|
||||
}
|
||||
|
||||
const results = document.querySelectorAll(qs);
|
||||
const count = results.length;
|
||||
if (count === 0) {
|
||||
NO_RESULTS_NOTICE.classList.remove(`d-none`);
|
||||
SUBMIT_PRODUCT.classList.add("d-none");
|
||||
} else {
|
||||
NO_RESULTS_NOTICE.classList.add(`d-none`);
|
||||
SUBMIT_PRODUCT.classList.remove("d-none");
|
||||
}
|
||||
},
|
||||
|
||||
test: (product, text) => {
|
||||
text = text.toLowerCase(); // Note that this is absolutely not true for all languages, but it's true for us.
|
||||
let qs, data;
|
||||
|
||||
for (const field of FILTERS) {
|
||||
qs = `.product-${field}`;
|
||||
data = product.querySelector(qs);
|
||||
data = (data.value || data.textContent).toLowerCase();
|
||||
if (data.indexOf(text) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
testCategories: (product, category) => {
|
||||
if (category === "None") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const productCategories = Array.from(
|
||||
product.querySelectorAll(".product-categories")
|
||||
);
|
||||
|
||||
return productCategories.map((c) => c.value.trim()).includes(category);
|
||||
},
|
||||
};
|
||||
|
||||
// TODO: turn this into a static class as well
|
||||
const PNIToggle = {
|
||||
init: () => {
|
||||
if (!toggle) {
|
||||
return console.warn(
|
||||
`Could not find the PNI filter checkbox. PNI filtering will not be available.`
|
||||
);
|
||||
}
|
||||
|
||||
toggle.addEventListener(`change`, (evt) => {
|
||||
const filter = evt.target.checked;
|
||||
|
||||
// TODO: this might be an A/B testing opportunity to see
|
||||
// whether users assume this toggle is a navigation
|
||||
// action or not?
|
||||
history.replaceState(
|
||||
{
|
||||
...history.state,
|
||||
filter,
|
||||
},
|
||||
SearchFilter.getTitle(categoryTitle.value.trim()),
|
||||
location.href
|
||||
);
|
||||
|
||||
if (filter) {
|
||||
document.body.classList.add(`show-ding-only`);
|
||||
} else {
|
||||
document.body.classList.remove(`show-ding-only`);
|
||||
}
|
||||
|
||||
if (SearchFilter.searchInput.value.trim()) {
|
||||
SearchFilter.searchInput.focus();
|
||||
SearchFilter.checkForEmptyNotice();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// bootstrap both searching and privacy-ding-filtering
|
||||
SearchFilter.init();
|
||||
PNIToggle.init();
|
||||
const searchFilter = new SearchFilter();
|
||||
new PNIToggle(searchFilter);
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
- [x] land Playwright testing
|
||||
- [x] create PNI search test(s)
|
||||
- [x] move all `init()` calls out as plain functions
|
||||
- [x] updated the base data to include subcategories for a PNI main category.
|
||||
- [x] update tests to check for subcategory behaviour.
|
||||
- [x] continue the refactor
|
||||
- [x] move all "object functions" in search over as (temporary) "class functions"
|
||||
- [x] move all "can be moved to utils" functions into their own (possibly more than one) util modules
|
||||
- [x] move the setupXYZ functions back into the class (where sensible)
|
||||
- [x] we should be done with the initial refactor now
|
||||
|
||||
|
||||
Outstanding tasks:
|
||||
- check if we can create a single history update function, as we call build and set state in several different places at the moment.
|
||||
- try to move as many consts into classes/constructors rather than keeping them bare consts
|
|
@ -0,0 +1,25 @@
|
|||
const CREEPINESS_FACE = document.querySelector(".creep-o-meter-information");
|
||||
|
||||
export class CreepUtils {
|
||||
static moveCreepyFace() {
|
||||
// When searching, check to see how many products are still visible
|
||||
// If there are no visible products, there are "no search results"
|
||||
// And when there are no search results, do not show the creepo-meter-face
|
||||
if (document.querySelectorAll(".product-box:not(.d-none)").length) {
|
||||
// If there are search results, show the creepo-meter-face
|
||||
CREEPINESS_FACE.classList.remove("d-none");
|
||||
} else {
|
||||
// If there are no search results, hide the creepo-meter-face
|
||||
CREEPINESS_FACE.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
static sortOnCreepiness() {
|
||||
const container = document.querySelector(`.product-box-list`);
|
||||
const list = [...container.querySelectorAll(`.product-box`)];
|
||||
const creepVal = (e) => parseFloat(e.dataset.creepiness);
|
||||
list
|
||||
.sort((a, b) => creepVal(a) - creepVal(b))
|
||||
.forEach((p) => container.append(p));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
import { Utils } from "./utils.js";
|
||||
import { CreepUtils } from "./creep-utils.js";
|
||||
|
||||
const categoryTitle = document.querySelector(`.category-title`);
|
||||
const parentTitle = document.querySelector(`.parent-title`);
|
||||
const toggle = document.querySelector(`#product-filter-pni-toggle`);
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @param {*} instance
|
||||
* @param {*} searchBar
|
||||
* @param {*} searchInput
|
||||
*/
|
||||
export function setupHistoryManagement(instance, searchBar, searchInput) {
|
||||
setupPopStateHandler(instance, searchBar, searchInput);
|
||||
performInitialHistoryReplace(instance, searchBar, searchInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @param {*} instance
|
||||
* @param {*} searchBar
|
||||
* @param {*} searchInput
|
||||
*/
|
||||
export function performInitialHistoryReplace(instance, searchBar, searchInput) {
|
||||
history.replaceState(
|
||||
{
|
||||
title: Utils.getTitle(categoryTitle.value.trim()),
|
||||
category: categoryTitle.value.trim(),
|
||||
parent: parentTitle.value.trim(),
|
||||
search: history.state?.search ?? "",
|
||||
filter: history.state?.filter,
|
||||
},
|
||||
Utils.getTitle(categoryTitle.value.trim()),
|
||||
location.href
|
||||
);
|
||||
|
||||
if (history.state?.search) {
|
||||
searchBar.classList.add(`has-content`);
|
||||
searchInput.value = history.state?.search;
|
||||
instance.filter(history.state?.search);
|
||||
} else {
|
||||
searchBar.classList.remove(`has-content`);
|
||||
searchInput.value = ``;
|
||||
}
|
||||
|
||||
if (history.state?.filter) {
|
||||
toggle.checked = history.state?.filter;
|
||||
|
||||
if (history.state?.filter) {
|
||||
document.body.classList.add(`show-ding-only`);
|
||||
} else {
|
||||
document.body.classList.remove(`show-ding-only`);
|
||||
}
|
||||
}
|
||||
|
||||
if (history.state?.parent && history.state?.category) {
|
||||
document
|
||||
.querySelector(`a.subcategories[data-name="${history.state?.category}"]`)
|
||||
.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "start",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @param {*} instance
|
||||
* @param {*} searchBar
|
||||
* @param {*} searchInput
|
||||
*/
|
||||
export function setupPopStateHandler(instance, searchBar, searchInput) {
|
||||
window.addEventListener(`popstate`, (event) => {
|
||||
const { state } = event;
|
||||
if (!state) return; // if it's a "real" back, we shouldn't need to do anything
|
||||
|
||||
const { title, category, parent } = state;
|
||||
document.title = title;
|
||||
|
||||
if (!history.state?.search) {
|
||||
instance.clearCategories();
|
||||
categoryTitle.value = category;
|
||||
parentTitle.value = parent;
|
||||
|
||||
searchBar.classList.remove(`has-content`);
|
||||
searchInput.value = ``;
|
||||
|
||||
if (parent) {
|
||||
Utils.highlightParentCategory();
|
||||
instance.toggleSubcategory();
|
||||
} else {
|
||||
document
|
||||
.querySelector(`#multipage-nav a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#multipage-nav a[data-name="${category}"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a[data-name="${category}"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
instance.toggleSubcategory(true);
|
||||
}
|
||||
} else {
|
||||
instance.toggleSubcategory(true);
|
||||
searchBar.classList.add(`has-content`);
|
||||
searchInput.value = history.state?.search;
|
||||
instance.filter(history.state?.search);
|
||||
}
|
||||
|
||||
instance.filterCategory(category);
|
||||
instance.filterSubcategory(parent || category);
|
||||
Utils.updateHeader(category, parent);
|
||||
|
||||
if (history.state?.filter) {
|
||||
toggle.checked = history.state?.filter;
|
||||
|
||||
if (history.state?.filter) {
|
||||
document.body.classList.add(`show-ding-only`);
|
||||
} else {
|
||||
document.body.classList.remove(`show-ding-only`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @param {*} instance
|
||||
*/
|
||||
export function applyHistory(instance) {
|
||||
const { category, parent } = history.state;
|
||||
|
||||
categoryTitle.value = category;
|
||||
parentTitle.value = parent;
|
||||
|
||||
if (parent) {
|
||||
Utils.highlightParentCategory();
|
||||
instance.toggleSubcategory();
|
||||
} else {
|
||||
document
|
||||
.querySelector(`#multipage-nav a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#multipage-nav a[data-name="${category}"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a[data-name="${category}"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
instance.toggleSubcategory(true);
|
||||
}
|
||||
|
||||
instance.filterCategory(category);
|
||||
instance.filterSubcategory(parent || category);
|
||||
Utils.updateHeader(category, parent);
|
||||
CreepUtils.sortOnCreepiness();
|
||||
CreepUtils.moveCreepyFace();
|
||||
|
||||
if (history.state?.parent && history.state?.category) {
|
||||
document
|
||||
.querySelector(`a.subcategories[data-name="${history.state?.category}"]`)
|
||||
.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "start",
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
import { Utils } from "./utils.js";
|
||||
|
||||
const categoryTitle = document.querySelector(`.category-title`);
|
||||
const parentTitle = document.querySelector(`.parent-title`);
|
||||
const subcategories = document.querySelectorAll(`.subcategories`);
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @param {*} instance
|
||||
*/
|
||||
export function setupNavLinks(instance) {
|
||||
const navLinks = document.querySelectorAll(
|
||||
`#multipage-nav a,.category-header,#pni-nav-mobile a`
|
||||
);
|
||||
|
||||
for (const nav of navLinks) {
|
||||
nav.addEventListener("click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
|
||||
if (evt.shiftKey || evt.metaKey || evt.ctrlKey || evt.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
document
|
||||
.querySelector(`#multipage-nav a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector("#pni-nav-mobile .dropdown-nav")
|
||||
.classList.remove("dropdown-nav-open");
|
||||
|
||||
const { name: categoryName } = evt.target.dataset ?? {};
|
||||
|
||||
if (categoryName) {
|
||||
document
|
||||
.querySelector(
|
||||
`#multipage-nav a[data-name="${categoryName}"]`
|
||||
)
|
||||
.classList.add(`active`);
|
||||
|
||||
document
|
||||
.querySelector(
|
||||
`#pni-nav-mobile a[data-name="${categoryName}"]`
|
||||
)
|
||||
.classList.add(`active`);
|
||||
|
||||
instance.clearText();
|
||||
history.pushState(
|
||||
{
|
||||
title: Utils.getTitle(categoryName),
|
||||
category: categoryName,
|
||||
parent: "",
|
||||
search: "",
|
||||
filter: history.state?.filter,
|
||||
},
|
||||
Utils.getTitle(categoryName),
|
||||
evt.target.href
|
||||
);
|
||||
|
||||
document.title = Utils.getTitle(categoryName);
|
||||
instance.filterSubcategory(categoryName);
|
||||
instance.toggleSubcategory(true);
|
||||
Utils.updateHeader(categoryName, "");
|
||||
instance.filterCategory(categoryName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const subcategory of subcategories) {
|
||||
subcategory.addEventListener(
|
||||
"click",
|
||||
(evt) => {
|
||||
evt.stopImmediatePropagation();
|
||||
if (evt.shiftKey || evt.metaKey || evt.ctrlKey || evt.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
let href;
|
||||
|
||||
const { name: subcategoryName } = evt.target.dataset;
|
||||
|
||||
if (subcategoryName) {
|
||||
instance.clearText();
|
||||
if (categoryTitle.value.trim() !== subcategoryName) {
|
||||
categoryTitle.value = subcategoryName;
|
||||
parentTitle.value = evt.target.dataset.parent;
|
||||
href = evt.target.href;
|
||||
instance.toggleSubcategory();
|
||||
Utils.highlightParentCategory();
|
||||
} else {
|
||||
categoryTitle.value = evt.target.dataset.parent;
|
||||
parentTitle.value = "";
|
||||
href = document.querySelector(
|
||||
`#multipage-nav a[data-name="${evt.target.dataset.parent}"]`
|
||||
).href;
|
||||
instance.toggleSubcategory(true);
|
||||
}
|
||||
|
||||
history.pushState(
|
||||
{
|
||||
title: Utils.getTitle(subcategoryName),
|
||||
category: categoryTitle.value.trim(),
|
||||
parent: parentTitle.value.trim(),
|
||||
search: "",
|
||||
filter: history.state?.filter,
|
||||
},
|
||||
Utils.getTitle(subcategoryName),
|
||||
href
|
||||
);
|
||||
|
||||
document.title = Utils.getTitle(categoryTitle.value.trim());
|
||||
Utils.updateHeader(
|
||||
categoryTitle.value.trim(),
|
||||
parentTitle.value.trim()
|
||||
);
|
||||
instance.filterCategory(categoryTitle.value.trim());
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @param {*} instance
|
||||
*/
|
||||
export function setupGoBackToAll(instance) {
|
||||
document
|
||||
.querySelector(`.go-back-to-all-link`)
|
||||
.addEventListener("click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
|
||||
if (evt.shiftKey || evt.metaKey || evt.ctrlKey || evt.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
instance.clearText();
|
||||
history.pushState(
|
||||
{
|
||||
title: Utils.getTitle("None"),
|
||||
category: "None",
|
||||
parent: "",
|
||||
search: "",
|
||||
filter: history.state?.filter,
|
||||
},
|
||||
Utils.getTitle(evt.target.dataset.name),
|
||||
evt.target.href
|
||||
);
|
||||
|
||||
document
|
||||
.querySelector(`#multipage-nav a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a.active`)
|
||||
.classList.remove(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#multipage-nav a[data-name="None"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a[data-name="None"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
instance.filterCategory("None");
|
||||
parentTitle.value = "";
|
||||
});
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { Utils } from "./utils.js";
|
||||
|
||||
export class PNIToggle {
|
||||
constructor(searchFilter) {
|
||||
this.searchFilter = searchFilter;
|
||||
this.categoryTitle = document.querySelector(`.category-title`);
|
||||
this.toggle = document.querySelector(`#product-filter-pni-toggle`);
|
||||
|
||||
if (!this.toggle) {
|
||||
// TODO: this should become a throw, with enough integration tests that
|
||||
// we can be confident that any page that should have it, has it,
|
||||
// failing our tests on pages that should but don't.
|
||||
return console.warn(
|
||||
`Could not find the PNI filter checkbox. PNI filtering will not be available.`
|
||||
);
|
||||
}
|
||||
|
||||
this.toggle.addEventListener(`change`, (evt) => {
|
||||
const doFilter = !!evt.target.checked;
|
||||
this.togglePrivacyOnly(doFilter);
|
||||
});
|
||||
}
|
||||
|
||||
togglePrivacyOnly(doFilter) {
|
||||
const { searchFilter, categoryTitle } = this;
|
||||
|
||||
// TODO: this might be an A/B testing opportunity to see
|
||||
// whether users assume this toggle is a navigation
|
||||
// action or not?
|
||||
const state = { ...history.state, filter: doFilter };
|
||||
const title = Utils.getTitle(categoryTitle.value.trim());
|
||||
history.replaceState(state, title, location.href);
|
||||
|
||||
const toggleFunction = doFilter ? `add` : `remove`;
|
||||
document.body.classList[toggleFunction](`show-ding-only`);
|
||||
|
||||
if (searchFilter.searchInput.value.trim()) {
|
||||
searchFilter.searchInput.focus();
|
||||
Utils.checkForEmptyNotice();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
import { Utils } from "./utils.js";
|
||||
import { CreepUtils } from "./creep-utils.js";
|
||||
import { markScrollStart } from "./slider-area.js";
|
||||
import { setupHistoryManagement, applyHistory } from "./history.js";
|
||||
import { setupNavLinks, setupGoBackToAll } from "./member-functions.js";
|
||||
|
||||
/**
|
||||
* ...
|
||||
*/
|
||||
export class SearchFilter {
|
||||
constructor() {
|
||||
const { searchBar, searchInput } = this.setupSearchBar();
|
||||
setupNavLinks(this);
|
||||
setupGoBackToAll(this);
|
||||
setupHistoryManagement(this, searchBar, searchInput);
|
||||
|
||||
const subContainer = document.querySelector(`.subcategory-header`);
|
||||
[`mousedown`, `touchstart`].forEach((type) =>
|
||||
subContainer.addEventListener(type, markScrollStart)
|
||||
);
|
||||
|
||||
this.allProducts = document.querySelectorAll(`figure.product-box`);
|
||||
this.categoryTitle = document.querySelector(`.category-title`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the search filter functionality, and return the
|
||||
* searchbar and searchinput elements for external function
|
||||
* binding purposes.
|
||||
*/
|
||||
setupSearchBar() {
|
||||
const searchBar = (this.searchBar = document.querySelector(
|
||||
`#product-filter-search`
|
||||
));
|
||||
|
||||
if (!searchBar) {
|
||||
return console.warn(
|
||||
`Could not find the PNI search bar. Search will not be available.`
|
||||
);
|
||||
}
|
||||
|
||||
const searchInput = (this.searchInput = searchBar.querySelector(`input`));
|
||||
|
||||
searchInput.addEventListener(`input`, (evt) => {
|
||||
const searchText = searchInput.value.trim();
|
||||
|
||||
if (searchText) {
|
||||
searchBar.classList.add(`has-content`);
|
||||
this.filter(searchText);
|
||||
} else {
|
||||
this.clearText();
|
||||
applyHistory(this);
|
||||
}
|
||||
});
|
||||
|
||||
const clear = searchBar.querySelector(`.clear-icon`);
|
||||
if (!clear) {
|
||||
return console.warn(
|
||||
`Could not find the PNI search input clear icon. Search will work, but clearing will not.`
|
||||
);
|
||||
}
|
||||
|
||||
clear.addEventListener(`click`, (evt) => {
|
||||
evt.preventDefault();
|
||||
searchInput.focus();
|
||||
this.clearText();
|
||||
applyHistory(this);
|
||||
});
|
||||
|
||||
return { searchBar, searchInput };
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the search text
|
||||
*/
|
||||
clearText() {
|
||||
const { searchBar, searchInput } = this;
|
||||
searchBar.classList.remove(`has-content`);
|
||||
searchInput.value = ``;
|
||||
|
||||
this.allProducts.forEach((product) => {
|
||||
product.classList.remove(`d-none`);
|
||||
product.classList.add(`d-flex`);
|
||||
});
|
||||
|
||||
CreepUtils.sortOnCreepiness();
|
||||
CreepUtils.moveCreepyFace();
|
||||
|
||||
const state = { ...history.state, search: "" };
|
||||
const title = Utils.getTitle(this.categoryTitle.value.trim());
|
||||
history.replaceState(state, title, location.href);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searching is really "filtering", toggling visibility for products
|
||||
* based on whether they match the given text (from amongst several
|
||||
* possible product fields).
|
||||
* @param {*} text
|
||||
*/
|
||||
filter(text) {
|
||||
this.clearCategories();
|
||||
this.toggleSubcategory(true);
|
||||
this.filterSubcategory("None");
|
||||
|
||||
Utils.updateHeader("None", null);
|
||||
Utils.selectAllCategory();
|
||||
Utils.toggleProducts(text);
|
||||
|
||||
const state = { ...history.state, search: text };
|
||||
const title = Utils.getTitle(this.categoryTitle.value.trim());
|
||||
history.replaceState(state, title, location.href);
|
||||
|
||||
Utils.sortFilteredProducts();
|
||||
CreepUtils.moveCreepyFace();
|
||||
Utils.checkForEmptyNotice();
|
||||
}
|
||||
|
||||
clearCategories() {
|
||||
const parentTitle = document.querySelector(`.parent-title`);
|
||||
parentTitle.value = null;
|
||||
this.filterCategory("None");
|
||||
Utils.clearCategories();
|
||||
}
|
||||
|
||||
filterCategory(category) {
|
||||
Utils.showProductsForCategory(category);
|
||||
this.categoryTitle.value = category;
|
||||
CreepUtils.sortOnCreepiness();
|
||||
CreepUtils.moveCreepyFace();
|
||||
Utils.checkForEmptyNotice();
|
||||
}
|
||||
|
||||
toggleSubcategory(clear = false) {
|
||||
const activeClasses = [
|
||||
"active",
|
||||
"tw-bg-gray-80",
|
||||
"tw-text-white",
|
||||
"tw-border-gray-80",
|
||||
];
|
||||
|
||||
const defaultClasses = [
|
||||
"hover:tw-border-pni-lilac",
|
||||
"hover:tw-bg-pni-lilac",
|
||||
"tw-text-gray-60",
|
||||
"tw-border-gray-20",
|
||||
"tw-bg-white",
|
||||
];
|
||||
|
||||
if (document.querySelector(`a.subcategories.active`)) {
|
||||
document
|
||||
.querySelector(`a.subcategories.active`)
|
||||
.classList.add(...defaultClasses);
|
||||
document
|
||||
.querySelector(`a.subcategories.active`)
|
||||
.classList.remove(...activeClasses);
|
||||
}
|
||||
|
||||
if (clear === true) return;
|
||||
|
||||
this.activateSubcategory(activeClasses, defaultClasses);
|
||||
}
|
||||
|
||||
activateSubcategory(activeClasses, defaultClasses) {
|
||||
const categoryName = this.categoryTitle.value.trim();
|
||||
|
||||
document
|
||||
.querySelector(`a.subcategories[data-name="${categoryName}"]`)
|
||||
.classList.add(...activeClasses);
|
||||
|
||||
document
|
||||
.querySelector(`a.subcategories[data-name="${categoryName}"]`)
|
||||
.classList.remove(...defaultClasses);
|
||||
|
||||
document
|
||||
.querySelector(`a.subcategories[data-name="${categoryName}"]`)
|
||||
.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "start",
|
||||
});
|
||||
}
|
||||
|
||||
filterSubcategory(category) {
|
||||
const subcategories = document.querySelectorAll(`.subcategories`);
|
||||
for (const subcategory of subcategories) {
|
||||
if (subcategory.dataset.parent === category) {
|
||||
subcategory.classList.remove(`tw-hidden`);
|
||||
} else {
|
||||
subcategory.classList.add(`tw-hidden`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* mouse/touch scroll functionality for the category area
|
||||
* @param {*} event
|
||||
*/
|
||||
|
||||
const subcategories = document.querySelectorAll(`.subcategories`);
|
||||
const subContainer = document.querySelector(`.subcategory-header`);
|
||||
const subClasses = subContainer.classList;
|
||||
|
||||
let pos = { left: 0, x: 0 };
|
||||
|
||||
function stop(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
export function markScrollStart(event) {
|
||||
stop(event);
|
||||
subClasses.add("cursor-grabbing", "select-none");
|
||||
|
||||
pos = {
|
||||
left: subContainer.scrollLeft,
|
||||
x: event.clientX,
|
||||
};
|
||||
|
||||
[`mousemove`, `touchmove`].forEach((type) =>
|
||||
document.addEventListener(type, markScrollMove)
|
||||
);
|
||||
|
||||
[`mouseup`, `touchend`, `touchcancel`].forEach((type) =>
|
||||
document.addEventListener(type, markScrollEnd)
|
||||
);
|
||||
}
|
||||
|
||||
function markScrollMove(event) {
|
||||
subcategories.forEach((subcategory) => {
|
||||
subcategory.classList.add("pointer-events-none");
|
||||
});
|
||||
const dx = event.clientX - pos.x;
|
||||
subContainer.scrollLeft = pos.left - dx;
|
||||
}
|
||||
|
||||
function markScrollEnd(event) {
|
||||
stop(event);
|
||||
|
||||
subcategories.forEach((subcategory) => {
|
||||
subcategory.classList.remove("pointer-events-none");
|
||||
});
|
||||
|
||||
subClasses.remove("cursor-grabbing", "select-none");
|
||||
|
||||
[`mousemove`, `touchmove`].forEach((type) =>
|
||||
document.removeEventListener(type, markScrollMove)
|
||||
);
|
||||
|
||||
[`mouseup`, `touchend`, `touchcancel`].forEach((type) =>
|
||||
document.removeEventListener(type, markScrollEnd)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,263 @@
|
|||
const SORTS = [`name`, `company`, `blurb`];
|
||||
const FILTERS = [`company`, `name`, `blurb`, `worst-case`];
|
||||
const ALL_PRODUCTS = document.querySelectorAll(`figure.product-box`);
|
||||
const SUBMIT_PRODUCT = document.querySelector(".recommend-product");
|
||||
const NO_RESULTS_NOTICE = document.getElementById(
|
||||
`product-filter-no-results-notice`
|
||||
);
|
||||
const ALL_CATEGORY_LABEL = document.querySelector(
|
||||
`#multipage-nav .multipage-link[data-name="None"]`
|
||||
).textContent;
|
||||
const PARENT_TITLE = document.querySelector(`.parent-title`);
|
||||
|
||||
|
||||
export class Utils {
|
||||
/**
|
||||
*...
|
||||
* @param {*} category
|
||||
* @returns
|
||||
*/
|
||||
static getTitle(category) {
|
||||
if (category == "None")
|
||||
return document.querySelector('meta[name="pni-home-title"]').content;
|
||||
else {
|
||||
return `${category} | ${
|
||||
document.querySelector('meta[name="pni-category-title"]').content
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @param {*} category
|
||||
* @param {*} parent
|
||||
*/
|
||||
static updateHeader(category, parent) {
|
||||
if (parent) {
|
||||
document.querySelector(".category-header").textContent = parent;
|
||||
document.querySelector(".category-header").dataset.name = parent;
|
||||
document.querySelector(".category-header").href = document.querySelector(
|
||||
`#multipage-nav a[data-name="${parent}"]`
|
||||
).href;
|
||||
document.querySelector(`#pni-nav-mobile .active-link-label`).textContent =
|
||||
parent;
|
||||
} else {
|
||||
const header = category === "None" ? ALL_CATEGORY_LABEL : category;
|
||||
document.querySelector(".category-header").textContent = header;
|
||||
document.querySelector(".category-header").dataset.name = category;
|
||||
document.querySelector(".category-header").href = document.querySelector(
|
||||
`#multipage-nav a[data-name="${category}"]`
|
||||
).href;
|
||||
document.querySelector(`#pni-nav-mobile .active-link-label`).textContent =
|
||||
category === "None"
|
||||
? document.querySelector(`#multipage-nav a[data-name="None"]`)
|
||||
.textContent
|
||||
: category;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
*/
|
||||
static clearCategories() {
|
||||
if (document.querySelector(`#multipage-nav a.active`)) {
|
||||
document
|
||||
.querySelector(`#multipage-nav a.active`)
|
||||
.classList.remove(`active`);
|
||||
document
|
||||
.querySelector(`#multipage-nav a[data-name="None"]`)
|
||||
.classList.add(`active`);
|
||||
}
|
||||
|
||||
if (document.querySelector(`#pni-nav-mobile a.active`)) {
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a.active`)
|
||||
.classList.remove(`active`);
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a[data-name="None"]`)
|
||||
.classList.add(`active`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
*/
|
||||
static selectAllCategory() {
|
||||
if (document.querySelector(`#multipage-nav a.active`)) {
|
||||
document
|
||||
.querySelector(`#multipage-nav a.active`)
|
||||
.classList.remove(`active`);
|
||||
}
|
||||
|
||||
if (document.querySelector(`#pni-nav-mobile a.active`)) {
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a.active`)
|
||||
.classList.remove(`active`);
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector(`#multipage-nav a[data-name="None"]`)
|
||||
.classList.add(`active`);
|
||||
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a[data-name="None"]`)
|
||||
.classList.add(`active`);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
static highlightParentCategory() {
|
||||
if (document.querySelector(`#multipage-nav a.active`)) {
|
||||
document
|
||||
.querySelector(`#multipage-nav a.active`)
|
||||
.classList.remove(`active`);
|
||||
}
|
||||
|
||||
if (document.querySelector(`#pni-nav-mobile a.active`)) {
|
||||
document
|
||||
.querySelector(`#pni-nav-mobile a.active`)
|
||||
.classList.remove(`active`);
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector( `#pni-nav-mobile a[data-name="${PARENT_TITLE.value.trim()}"]`
|
||||
)
|
||||
.classList.add(`active`);
|
||||
|
||||
document
|
||||
.querySelector( `#multipage-nav a[data-name="${PARENT_TITLE.value.trim()}"]`
|
||||
)
|
||||
.classList.add(`active`);
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @param {*} text
|
||||
*/
|
||||
static toggleProducts(text) {
|
||||
ALL_PRODUCTS.forEach((product) => {
|
||||
if (this.test(product, text)) {
|
||||
product.classList.remove(`d-none`);
|
||||
product.classList.add(`d-flex`);
|
||||
} else {
|
||||
product.classList.add(`d-none`);
|
||||
product.classList.remove(`d-flex`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @param {*} category
|
||||
*/
|
||||
static showProductsForCategory(category) {
|
||||
ALL_PRODUCTS.forEach((product) => {
|
||||
if (this.testCategories(product, category)) {
|
||||
product.classList.remove(`d-none`);
|
||||
product.classList.add(`d-flex`);
|
||||
} else {
|
||||
product.classList.add(`d-none`);
|
||||
product.classList.remove(`d-flex`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @param {*} product
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
static test(product, text) {
|
||||
// Note that the following is absolutely not true for all
|
||||
// languages, but it's true for the ones we use.
|
||||
text = text.toLowerCase();
|
||||
let qs, data;
|
||||
|
||||
for (const field of FILTERS) {
|
||||
qs = `.product-${field}`;
|
||||
data = product.querySelector(qs);
|
||||
data = (data.value || data.textContent).toLowerCase();
|
||||
if (data.indexOf(text) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @param {*} product
|
||||
* @param {*} category
|
||||
* @returns
|
||||
*/
|
||||
static testCategories(product, category) {
|
||||
if (category === "None") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const productCategories = Array.from(
|
||||
product.querySelectorAll(".product-categories")
|
||||
);
|
||||
|
||||
return productCategories.map((c) => c.value.trim()).includes(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
*/
|
||||
static sortFilteredProducts() {
|
||||
const container = document.querySelector(`.product-box-list`);
|
||||
const list = [...container.querySelectorAll(`.product-box`)];
|
||||
|
||||
list.sort((a, b) => {
|
||||
for (const field of SORTS) {
|
||||
const qs = `.product-${field}`;
|
||||
const [propertyA, propertyB] = [
|
||||
a.querySelector(qs),
|
||||
b.querySelector(qs),
|
||||
];
|
||||
const [propertyNameA, propertyNameB] = [
|
||||
(propertyA.value || propertyA.textContent).toLowerCase(),
|
||||
(propertyB.value || propertyB.textContent).toLowerCase(),
|
||||
];
|
||||
|
||||
if (
|
||||
propertyNameA !== propertyNameB ||
|
||||
field === SORTS[SORTS.length - 1]
|
||||
) {
|
||||
return propertyNameA < propertyNameB
|
||||
? -1
|
||||
: propertyNameA > propertyNameB
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
list.forEach((p) => container.append(p));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
static checkForEmptyNotice() {
|
||||
let qs = `figure.product-box:not(.d-none)`;
|
||||
|
||||
if (document.body.classList.contains(`show-ding-only`)) {
|
||||
qs = `${qs}.privacy-ding`;
|
||||
}
|
||||
|
||||
const results = document.querySelectorAll(qs);
|
||||
|
||||
if (results.length === 0) {
|
||||
NO_RESULTS_NOTICE.classList.remove(`d-none`);
|
||||
SUBMIT_PRODUCT.classList.add("d-none");
|
||||
} else {
|
||||
NO_RESULTS_NOTICE.classList.add(`d-none`);
|
||||
SUBMIT_PRODUCT.classList.remove("d-none");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,3 +32,140 @@ test(`Foundation homepage`, async ({ page }, testInfo) => {
|
|||
expect(await countryPicker.isVisible()).toBe(true);
|
||||
expect(await languagePicker.isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
/**
|
||||
* Perform several PNI tests related to searching/filtering
|
||||
*
|
||||
* NOTE: this requires a `new-db` run with the seed value set
|
||||
* through RANDOM_SEED=530910203 in your .env file
|
||||
*/
|
||||
test(`PNI search`, async ({ page }, testInfo) => {
|
||||
page.on(`console`, console.log);
|
||||
await page.goto(`http://localhost:8000/en/privacynotincluded`);
|
||||
await page.locator(`body.react-loaded`);
|
||||
await waitForImagesToLoad(page);
|
||||
|
||||
const counts = {
|
||||
// provided RANDOM_SEED=530910203 was used!
|
||||
total: 41,
|
||||
health: 14,
|
||||
smart: 4,
|
||||
percy: 2,
|
||||
the: 14,
|
||||
theWithDing: 4,
|
||||
};
|
||||
|
||||
const qs = {
|
||||
ding: `#product-filter-pni-toggle`,
|
||||
dingLabel: `label[for="product-filter-pni-toggle"]`,
|
||||
products: `.product-box:visible`,
|
||||
searchBar: `#product-filter-search-input`,
|
||||
clearSearch: `label[for="product-filter-search-input"]`,
|
||||
activeCategory: `#multipage-nav a.multipage-link.active`,
|
||||
activeSubCategory: `a.subcategories.active`,
|
||||
healthCategory: `#multipage-nav a.multipage-link[data-name="Health & Exercise"]`,
|
||||
};
|
||||
|
||||
let products, activeCategory;
|
||||
|
||||
// verify that the PNI "ding" is not selected on initial load
|
||||
const ding = page.locator(qs.ding);
|
||||
await expect(ding).not.toBeChecked();
|
||||
|
||||
// Baseline product count test
|
||||
products = page.locator(qs.products);
|
||||
await expect(products).toHaveCount(counts.total);
|
||||
|
||||
// Verify that all products are sorted on creepiness
|
||||
expect(await confirmSorted(page)).toBe(true);
|
||||
|
||||
// Test search filtering for "percy": there should be two products
|
||||
const searchBar = page.locator(qs.searchBar);
|
||||
await searchBar.type("percy");
|
||||
products = page.locator(qs.products);
|
||||
await expect(products).toHaveCount(counts.percy);
|
||||
|
||||
// And we should be back to the original number when clearing search.
|
||||
await page.click(qs.clearSearch);
|
||||
products = page.locator(qs.products);
|
||||
await expect(products).toHaveCount(counts.total);
|
||||
|
||||
// Click through to the Health & Exercise category and
|
||||
// verify the correct number of products show up.
|
||||
await page.click(qs.healthCategory);
|
||||
activeCategory = page.locator(qs.activeCategory);
|
||||
await expect(activeCategory).toHaveAttribute(
|
||||
`data-name`,
|
||||
`Health & Exercise`
|
||||
);
|
||||
products = page.locator(qs.products);
|
||||
await expect(products).toHaveCount(counts.health);
|
||||
|
||||
// Select a known subcategory and verify the correct
|
||||
// number of products show up.
|
||||
subcats = page.locator(`a.subcategories`);
|
||||
await expect(subcats).toHaveCount(3);
|
||||
subcat = page.locator(`a.subcategories:nth-child(2)`);
|
||||
await expect(subcat).toHaveText(`Smart Scales`);
|
||||
await page.click(`a.subcategories:nth-child(2)`);
|
||||
products = page.locator(qs.products);
|
||||
await expect(products).toHaveCount(counts.smart);
|
||||
|
||||
// Filter for "percy" products: there should be two, and the active
|
||||
// category should be "all" while search filtering
|
||||
await searchBar.type("percy");
|
||||
products = page.locator(qs.products);
|
||||
await expect(products).toHaveCount(2);
|
||||
activeCategory = page.locator(qs.activeCategory);
|
||||
await expect(activeCategory).toHaveAttribute(`data-name`, `None`);
|
||||
|
||||
// Clear the search bar: original subcategory should be "active"
|
||||
await page.click(qs.clearSearch);
|
||||
activeCategory = page.locator(qs.activeCategory);
|
||||
await expect(activeCategory).toHaveAttribute(
|
||||
`data-name`,
|
||||
`Health & Exercise`
|
||||
);
|
||||
activeSubCat = page.locator(qs.activeSubCategory);
|
||||
await expect(activeSubCat).toHaveText(`Smart Scales`);
|
||||
products = page.locator(qs.products);
|
||||
await expect(products).toHaveCount(counts.smart);
|
||||
|
||||
// Clicking the subcategory should restore the parent category
|
||||
await page.click(qs.activeSubCategory);
|
||||
products = page.locator(qs.products);
|
||||
await expect(products).toHaveCount(counts.health);
|
||||
|
||||
// Filtering for "ding" should refocus on text field if there
|
||||
// was a search term, while filtering for ding-only
|
||||
await searchBar.type("the");
|
||||
await page.click(`main`);
|
||||
await page.click(qs.dingLabel);
|
||||
await expect(searchBar).toBeFocused();
|
||||
products = page.locator(qs.products);
|
||||
await expect(products).toHaveCount(counts.theWithDing);
|
||||
await page.click(qs.dingLabel);
|
||||
products = page.locator(qs.products);
|
||||
await expect(products).toHaveCount(counts.the);
|
||||
|
||||
// Finally, verify that all products are still sorted
|
||||
await page.click(qs.clearSearch);
|
||||
expect(await confirmSorted(page)).toBe(true);
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function for PNI search/filter tests.
|
||||
* Confirms whether or not all products are sorted by creepiness
|
||||
* @param {Page} page The Playwright page handle
|
||||
*/
|
||||
async function confirmSorted(page) {
|
||||
return page.evaluate(() => {
|
||||
const products = [...document.querySelectorAll(`.product-box`)];
|
||||
return products.every((e, i, list, a, b) => {
|
||||
if (i === 0) return true;
|
||||
a = parseFloat(list[i - 1].dataset.creepiness);
|
||||
b = parseFloat(e.dataset.creepiness);
|
||||
return b >= a;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче