chore: initialize project repository with core extension files
- Add .gitignore to exclude node_modules, dist, logs, and system files - Add comprehensive project documentation including README, deployment guide, and development setup - Add .kiro project specifications for amazon-product-bar-extension, appwrite-cloud-storage, appwrite-userid-repair, blacklist-feature, and enhanced-item-management - Add .kiro steering documents for product, structure, styling, and tech guidelines - Add VSCode settings configuration for consistent development environment - Add manifest.json and babel/vite configuration for extension build setup - Add complete source code implementation including AppWrite integration, storage managers, UI components, and services - Add comprehensive test suite with Jest configuration and 30+ test files covering all major modules - Add test HTML files for integration testing and validation - Add coverage reports and build validation scripts - Add AppWrite setup and repair documentation for database schema management - Add migration guides and responsive accessibility implementation documentation - Establish foundation for Amazon product bar extension with full feature set including blacklist management, enhanced item workflows, and real-time synchronization
This commit is contained in:
733
.kiro/specs/blacklist-feature/design.md
Normal file
733
.kiro/specs/blacklist-feature/design.md
Normal file
@@ -0,0 +1,733 @@
|
||||
# Design Document: Blacklist Feature
|
||||
|
||||
## Overview
|
||||
|
||||
Die Blacklist-Funktion erweitert die Amazon Product Bar Extension um die Möglichkeit, Markennamen zu verwalten und Produkte dieser Marken visuell in der Product_Bar zu kennzeichnen. Die Funktion nutzt einen neuen Menüpunkt "Blacklist" im StaggeredMenu, speichert Daten im Local Storage und zeigt Marken-Logos bei geblacklisteten Produkten an.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[StaggeredMenu] --> B[Blacklist Menu Item]
|
||||
B --> C[Blacklist Panel Manager]
|
||||
C --> D[Blacklist Storage Manager]
|
||||
D --> E[Local Storage]
|
||||
C --> F[Brand Input UI]
|
||||
C --> G[Brand List UI]
|
||||
|
||||
H[Product Card Detector] --> I[Brand Extractor]
|
||||
I --> J[Blacklist Matcher]
|
||||
J --> D
|
||||
J --> K[Brand Icon Manager]
|
||||
K --> L[Product Bar]
|
||||
|
||||
M[Brand Logo Registry] --> K
|
||||
```
|
||||
|
||||
Die Blacklist-Funktion besteht aus:
|
||||
1. **Blacklist Panel Manager** - UI-Verwaltung für das Blacklist-Panel
|
||||
2. **Blacklist Storage Manager** - CRUD-Operationen für geblacklistete Marken
|
||||
3. **Brand Extractor** - Extraktion von Markennamen aus Produktkarten
|
||||
4. **Blacklist Matcher** - Case-insensitive Vergleich von Marken
|
||||
5. **Brand Icon Manager** - Verwaltung der Marken-Icons in Product_Bars
|
||||
6. **Brand Logo Registry** - Vordefinierte Logos für bekannte Marken
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
### 1. Blacklist Storage Manager
|
||||
|
||||
```javascript
|
||||
// BlacklistStorageManager.js
|
||||
class BlacklistStorageManager {
|
||||
constructor() {
|
||||
this.STORAGE_KEY = 'amazon_ext_blacklist';
|
||||
}
|
||||
|
||||
// Speichert eine Marke in der Blacklist
|
||||
async addBrand(brandName) {
|
||||
const brands = await this.getBrands();
|
||||
const normalizedName = brandName.trim();
|
||||
|
||||
// Case-insensitive Duplikat-Check
|
||||
const exists = brands.some(b =>
|
||||
b.name.toLowerCase() === normalizedName.toLowerCase()
|
||||
);
|
||||
|
||||
if (exists) {
|
||||
throw new Error('Brand already exists');
|
||||
}
|
||||
|
||||
brands.push({
|
||||
id: this.generateId(),
|
||||
name: normalizedName,
|
||||
addedAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
await this.saveBrands(brands);
|
||||
return brands;
|
||||
}
|
||||
|
||||
// Holt alle geblacklisteten Marken
|
||||
async getBrands() {
|
||||
const data = localStorage.getItem(this.STORAGE_KEY);
|
||||
return data ? JSON.parse(data) : [];
|
||||
}
|
||||
|
||||
// Löscht eine Marke aus der Blacklist
|
||||
async deleteBrand(brandId) {
|
||||
const brands = await this.getBrands();
|
||||
const filtered = brands.filter(b => b.id !== brandId);
|
||||
await this.saveBrands(filtered);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// Prüft ob eine Marke geblacklistet ist (case-insensitive)
|
||||
async isBrandBlacklisted(brandName) {
|
||||
const brands = await this.getBrands();
|
||||
return brands.some(b =>
|
||||
b.name.toLowerCase() === brandName.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
// Speichert Marken im Local Storage
|
||||
async saveBrands(brands) {
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(brands));
|
||||
|
||||
// Event für UI-Updates emittieren
|
||||
if (window.amazonExtEventBus) {
|
||||
window.amazonExtEventBus.emit('blacklist:updated', brands);
|
||||
}
|
||||
}
|
||||
|
||||
generateId() {
|
||||
return 'bl_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Brand Extractor
|
||||
|
||||
```javascript
|
||||
// BrandExtractor.js
|
||||
class BrandExtractor {
|
||||
// Extrahiert Markennamen aus einer Produktkarte
|
||||
extractBrand(productCard) {
|
||||
// Methode 1: "by [Brand]" Text
|
||||
const byBrandElement = productCard.querySelector('.a-row.a-size-base.a-color-secondary');
|
||||
if (byBrandElement) {
|
||||
const byMatch = byBrandElement.textContent.match(/by\s+([^,\n]+)/i);
|
||||
if (byMatch) {
|
||||
return byMatch[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Methode 2: Brand-Link
|
||||
const brandLink = productCard.querySelector('a[href*="/stores/"], .a-link-normal[href*="brand="]');
|
||||
if (brandLink) {
|
||||
return brandLink.textContent.trim();
|
||||
}
|
||||
|
||||
// Methode 3: Aus Produkttitel extrahieren (erstes Wort oft die Marke)
|
||||
const titleElement = productCard.querySelector('h2 a span, .a-text-normal');
|
||||
if (titleElement) {
|
||||
const title = titleElement.textContent.trim();
|
||||
const firstWord = title.split(/\s+/)[0];
|
||||
// Nur wenn es wie ein Markenname aussieht (Großbuchstabe am Anfang)
|
||||
if (firstWord && /^[A-Z]/.test(firstWord)) {
|
||||
return firstWord;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Brand Logo Registry
|
||||
|
||||
```javascript
|
||||
// BrandLogoRegistry.js
|
||||
class BrandLogoRegistry {
|
||||
constructor() {
|
||||
// Vordefinierte SVG-Logos für bekannte Marken
|
||||
this.logos = {
|
||||
'nike': this.createNikeLogo(),
|
||||
'adidas': this.createAdidasLogo(),
|
||||
'puma': this.createPumaLogo(),
|
||||
'apple': this.createAppleLogo(),
|
||||
'samsung': this.createSamsungLogo()
|
||||
};
|
||||
|
||||
this.defaultBlockedIcon = this.createBlockedIcon();
|
||||
}
|
||||
|
||||
// Holt Logo für eine Marke (case-insensitive)
|
||||
getLogo(brandName) {
|
||||
const normalized = brandName.toLowerCase();
|
||||
return this.logos[normalized] || this.defaultBlockedIcon;
|
||||
}
|
||||
|
||||
// Prüft ob ein spezifisches Logo existiert
|
||||
hasLogo(brandName) {
|
||||
return brandName.toLowerCase() in this.logos;
|
||||
}
|
||||
|
||||
createNikeLogo() {
|
||||
return `<svg viewBox="0 0 16 16" width="16" height="16">
|
||||
<path fill="currentColor" d="M1.5 9.5c-.3.1-.5.4-.5.7 0 .2.1.4.3.5l.2.1c.1 0 .2 0 .3-.1l12-5.5c.2-.1.3-.3.3-.5 0-.3-.2-.5-.5-.6L1.5 9.5z"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
createAdidasLogo() {
|
||||
return `<svg viewBox="0 0 16 16" width="16" height="16">
|
||||
<path fill="currentColor" d="M2 12h3V6L2 12zm4 0h3V4L6 12zm4 0h3V2l-3 10z"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
createPumaLogo() {
|
||||
return `<svg viewBox="0 0 16 16" width="16" height="16">
|
||||
<path fill="currentColor" d="M8 2C4.7 2 2 4.7 2 8s2.7 6 6 6 6-2.7 6-6-2.7-6-6-6zm0 10c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
createAppleLogo() {
|
||||
return `<svg viewBox="0 0 16 16" width="16" height="16">
|
||||
<path fill="currentColor" d="M11.2 4.2c-.6-.7-1.4-1.1-2.3-1.2.1-.8.5-1.5 1.1-2-.6.1-1.2.4-1.6.9-.4-.5-1-.8-1.6-.9.6.5 1 1.2 1.1 2-.9.1-1.7.5-2.3 1.2C4.5 5.4 4 6.7 4 8c0 2.2 1.3 5 3 5 .5 0 .9-.2 1.3-.5.4.3.8.5 1.3.5 1.7 0 3-2.8 3-5 0-1.3-.5-2.6-1.4-3.8z"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
createSamsungLogo() {
|
||||
return `<svg viewBox="0 0 16 16" width="16" height="16">
|
||||
<rect fill="currentColor" x="2" y="6" width="12" height="4" rx="1"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
createBlockedIcon() {
|
||||
return `<svg viewBox="0 0 16 16" width="16" height="16">
|
||||
<circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<line x1="4" y1="4" x2="12" y2="12" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Brand Icon Manager
|
||||
|
||||
```javascript
|
||||
// BrandIconManager.js
|
||||
class BrandIconManager {
|
||||
constructor(blacklistStorage, brandExtractor, logoRegistry) {
|
||||
this.blacklistStorage = blacklistStorage;
|
||||
this.brandExtractor = brandExtractor;
|
||||
this.logoRegistry = logoRegistry;
|
||||
}
|
||||
|
||||
// Aktualisiert alle Product_Bars auf der Seite
|
||||
async updateAllBars() {
|
||||
const productBars = document.querySelectorAll('.amazon-ext-product-bar');
|
||||
const brands = await this.blacklistStorage.getBrands();
|
||||
const blacklistedNames = brands.map(b => b.name.toLowerCase());
|
||||
|
||||
productBars.forEach(bar => {
|
||||
const productCard = bar.closest('[data-asin]');
|
||||
if (!productCard) return;
|
||||
|
||||
const brand = this.brandExtractor.extractBrand(productCard);
|
||||
if (brand && blacklistedNames.includes(brand.toLowerCase())) {
|
||||
this.addBrandIcon(bar, brand);
|
||||
} else {
|
||||
this.removeBrandIcon(bar);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fügt Brand-Icon zu einer Product_Bar hinzu
|
||||
addBrandIcon(productBar, brandName) {
|
||||
let iconContainer = productBar.querySelector('.brand-icon');
|
||||
|
||||
if (!iconContainer) {
|
||||
iconContainer = document.createElement('div');
|
||||
iconContainer.className = 'brand-icon';
|
||||
productBar.insertBefore(iconContainer, productBar.firstChild);
|
||||
}
|
||||
|
||||
const logo = this.logoRegistry.getLogo(brandName);
|
||||
iconContainer.innerHTML = logo;
|
||||
iconContainer.title = `Blacklisted: ${brandName}`;
|
||||
iconContainer.style.display = 'flex';
|
||||
}
|
||||
|
||||
// Entfernt Brand-Icon von einer Product_Bar
|
||||
removeBrandIcon(productBar) {
|
||||
const iconContainer = productBar.querySelector('.brand-icon');
|
||||
if (iconContainer) {
|
||||
iconContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Fügt Icon zu allen Produkten einer bestimmten Marke hinzu
|
||||
async addIconForBrand(brandName) {
|
||||
const productBars = document.querySelectorAll('.amazon-ext-product-bar');
|
||||
|
||||
productBars.forEach(bar => {
|
||||
const productCard = bar.closest('[data-asin]');
|
||||
if (!productCard) return;
|
||||
|
||||
const brand = this.brandExtractor.extractBrand(productCard);
|
||||
if (brand && brand.toLowerCase() === brandName.toLowerCase()) {
|
||||
this.addBrandIcon(bar, brand);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Entfernt Icon von allen Produkten einer bestimmten Marke
|
||||
async removeIconForBrand(brandName) {
|
||||
const productBars = document.querySelectorAll('.amazon-ext-product-bar');
|
||||
|
||||
productBars.forEach(bar => {
|
||||
const productCard = bar.closest('[data-asin]');
|
||||
if (!productCard) return;
|
||||
|
||||
const brand = this.brandExtractor.extractBrand(productCard);
|
||||
if (brand && brand.toLowerCase() === brandName.toLowerCase()) {
|
||||
this.removeBrandIcon(bar);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Blacklist Panel Manager
|
||||
|
||||
```javascript
|
||||
// BlacklistPanelManager.js
|
||||
class BlacklistPanelManager {
|
||||
constructor(blacklistStorage, logoRegistry) {
|
||||
this.blacklistStorage = blacklistStorage;
|
||||
this.logoRegistry = logoRegistry;
|
||||
this.container = null;
|
||||
}
|
||||
|
||||
createBlacklistContent() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'amazon-ext-blacklist-content';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="blacklist-header">
|
||||
<h2>Blacklist</h2>
|
||||
<p class="blacklist-description">Markennamen hinzufügen, um Produkte zu markieren</p>
|
||||
</div>
|
||||
|
||||
<div class="add-brand-form">
|
||||
<input
|
||||
type="text"
|
||||
class="brand-input"
|
||||
placeholder="Markenname eingeben (z.B. Nike, Adidas)..."
|
||||
/>
|
||||
<button class="add-brand-btn">Hinzufügen</button>
|
||||
</div>
|
||||
|
||||
<div class="brand-list-container">
|
||||
<div class="brand-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="blacklist-message" style="display: none;"></div>
|
||||
`;
|
||||
|
||||
this.container = container;
|
||||
this.setupEventListeners();
|
||||
this.loadBrands();
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const input = this.container.querySelector('.brand-input');
|
||||
const addBtn = this.container.querySelector('.add-brand-btn');
|
||||
|
||||
addBtn.addEventListener('click', () => this.handleAddBrand());
|
||||
input.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') this.handleAddBrand();
|
||||
});
|
||||
}
|
||||
|
||||
async handleAddBrand() {
|
||||
const input = this.container.querySelector('.brand-input');
|
||||
const brandName = input.value.trim();
|
||||
|
||||
if (!brandName) {
|
||||
this.showMessage('Bitte einen Markennamen eingeben', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.blacklistStorage.addBrand(brandName);
|
||||
input.value = '';
|
||||
this.showMessage(`"${brandName}" zur Blacklist hinzugefügt`, 'success');
|
||||
this.loadBrands();
|
||||
} catch (error) {
|
||||
if (error.message === 'Brand already exists') {
|
||||
this.showMessage('Diese Marke ist bereits in der Blacklist', 'error');
|
||||
} else {
|
||||
this.showMessage('Fehler beim Speichern', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadBrands() {
|
||||
const brands = await this.blacklistStorage.getBrands();
|
||||
const listContainer = this.container.querySelector('.brand-list');
|
||||
|
||||
if (brands.length === 0) {
|
||||
listContainer.innerHTML = '<p class="empty-message">Keine Marken in der Blacklist</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
listContainer.innerHTML = brands.map(brand => `
|
||||
<div class="brand-item" data-id="${brand.id}">
|
||||
<div class="brand-logo">
|
||||
${this.logoRegistry.getLogo(brand.name)}
|
||||
</div>
|
||||
<span class="brand-name">${brand.name}</span>
|
||||
<button class="delete-brand-btn" data-id="${brand.id}">×</button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Delete-Button Event Listeners
|
||||
listContainer.querySelectorAll('.delete-brand-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const brandId = e.target.dataset.id;
|
||||
this.handleDeleteBrand(brandId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async handleDeleteBrand(brandId) {
|
||||
const brands = await this.blacklistStorage.getBrands();
|
||||
const brand = brands.find(b => b.id === brandId);
|
||||
|
||||
await this.blacklistStorage.deleteBrand(brandId);
|
||||
this.showMessage(`"${brand?.name}" entfernt`, 'success');
|
||||
this.loadBrands();
|
||||
}
|
||||
|
||||
showMessage(text, type) {
|
||||
const messageEl = this.container.querySelector('.blacklist-message');
|
||||
messageEl.textContent = text;
|
||||
messageEl.className = `blacklist-message ${type}`;
|
||||
messageEl.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
messageEl.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
showBlacklistPanel() {
|
||||
this.loadBrands();
|
||||
}
|
||||
|
||||
hideBlacklistPanel() {
|
||||
// Cleanup wenn nötig
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Data Models
|
||||
|
||||
### BlacklistedBrand
|
||||
|
||||
```typescript
|
||||
interface BlacklistedBrand {
|
||||
id: string; // Eindeutige ID (bl_timestamp_random)
|
||||
name: string; // Markenname (originale Schreibweise)
|
||||
addedAt: string; // ISO-Timestamp der Hinzufügung
|
||||
}
|
||||
```
|
||||
|
||||
### Local Storage Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"amazon_ext_blacklist": [
|
||||
{
|
||||
"id": "bl_1699123456789_abc123def",
|
||||
"name": "Nike",
|
||||
"addedAt": "2024-01-15T10:30:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "bl_1699123456790_xyz789ghi",
|
||||
"name": "Adidas",
|
||||
"addedAt": "2024-01-15T10:31:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Styling
|
||||
|
||||
```css
|
||||
/* Blacklist Panel Styles */
|
||||
.amazon-ext-blacklist-content {
|
||||
color: white;
|
||||
padding: 2rem;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.blacklist-header h2 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.blacklist-description {
|
||||
color: #888;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
|
||||
.add-brand-form {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.add-brand-form .brand-input {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #333;
|
||||
background: #222;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.add-brand-form .add-brand-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #ff9900;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.add-brand-form .add-brand-btn:hover {
|
||||
background: #e68a00;
|
||||
}
|
||||
|
||||
.brand-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.brand-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #222;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ff9900;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
flex: 1;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.delete-brand-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0 0.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.delete-brand-btn:hover {
|
||||
color: #ff4444;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
color: #666;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.blacklist-message {
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 4px;
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.blacklist-message.success {
|
||||
background: #1a4d1a;
|
||||
color: #4ade4a;
|
||||
}
|
||||
|
||||
.blacklist-message.error {
|
||||
background: #4d1a1a;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* Brand Icon in Product Bar */
|
||||
.amazon-ext-product-bar .brand-icon {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ff4444;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Correctness Properties
|
||||
|
||||
*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||||
|
||||
### Property 1: Brand Saving Round-Trip
|
||||
|
||||
*For any* valid brand name, saving it to the blacklist and then retrieving all brands should include that brand with the same name.
|
||||
|
||||
**Validates: Requirements 2.2, 2.3**
|
||||
|
||||
### Property 2: Case-Insensitive Comparison
|
||||
|
||||
*For any* two brand name strings that differ only in letter case (e.g., "Nike" vs "nike" vs "NIKE"), the `isBrandBlacklisted` function should return the same result for both.
|
||||
|
||||
**Validates: Requirements 4.1, 4.2**
|
||||
|
||||
### Property 3: Duplicate Prevention
|
||||
|
||||
*For any* brand name already in the blacklist, attempting to add a case-variant of that name should throw an error and not increase the blacklist size.
|
||||
|
||||
**Validates: Requirements 2.5, 4.3**
|
||||
|
||||
### Property 4: Whitespace Trimming
|
||||
|
||||
*For any* brand name with leading or trailing whitespace, the saved brand name should have no leading or trailing whitespace.
|
||||
|
||||
**Validates: Requirements 2.6**
|
||||
|
||||
### Property 5: Original Case Preservation
|
||||
|
||||
*For any* brand name saved to the blacklist, the retrieved brand name should preserve the exact original case as entered.
|
||||
|
||||
**Validates: Requirements 4.4**
|
||||
|
||||
### Property 6: Brand List Rendering Completeness
|
||||
|
||||
*For any* set of N saved brands, the rendered brand list should contain exactly N brand items.
|
||||
|
||||
**Validates: Requirements 3.1**
|
||||
|
||||
### Property 7: Logo Selection Consistency
|
||||
|
||||
*For any* brand name, if the brand has a predefined logo in the registry, `getLogo` should return that specific logo; otherwise, it should return the default blocked icon.
|
||||
|
||||
**Validates: Requirements 3.2, 6.3, 7.3, 7.4**
|
||||
|
||||
### Property 8: Delete Button Presence
|
||||
|
||||
*For any* rendered brand item in the blacklist panel, it should contain exactly one delete button element.
|
||||
|
||||
**Validates: Requirements 3.3**
|
||||
|
||||
### Property 9: Deletion Completeness
|
||||
|
||||
*For any* brand deleted from the blacklist, it should no longer appear in storage or in the UI after deletion.
|
||||
|
||||
**Validates: Requirements 3.4**
|
||||
|
||||
### Property 10: Brand Extraction Determinism
|
||||
|
||||
*For any* product card DOM element with brand information, the `extractBrand` function should return a non-null string representing the brand.
|
||||
|
||||
**Validates: Requirements 5.1, 5.2, 5.3**
|
||||
|
||||
### Property 11: No Marking Without Brand
|
||||
|
||||
*For any* product card where brand extraction returns null, no blacklist icon should be added to the product bar.
|
||||
|
||||
**Validates: Requirements 5.4**
|
||||
|
||||
### Property 12: Blacklist Icon Display
|
||||
|
||||
*For any* product whose extracted brand matches a blacklisted brand (case-insensitive), the product bar should display a brand icon.
|
||||
|
||||
**Validates: Requirements 6.1**
|
||||
|
||||
### Property 13: Real-Time Icon Updates
|
||||
|
||||
*For any* brand added to or removed from the blacklist, all visible product bars with matching brands should immediately reflect the change (icon added or removed).
|
||||
|
||||
**Validates: Requirements 6.4, 6.5**
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Handling |
|
||||
|----------|----------|
|
||||
| Empty brand name entered | Display error message, prevent saving |
|
||||
| Brand already exists | Display "already exists" message, prevent duplicate |
|
||||
| Local storage quota exceeded | Display warning, suggest cleanup |
|
||||
| Brand extraction fails | Skip blacklist marking for that product |
|
||||
| Invalid DOM structure | Graceful degradation, log warning |
|
||||
| Logo not found for brand | Use default blocked icon |
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- BlacklistStorageManager CRUD operations
|
||||
- Case-insensitive comparison logic
|
||||
- Whitespace trimming
|
||||
- BrandExtractor with various DOM structures
|
||||
- BrandLogoRegistry logo retrieval
|
||||
- BlacklistPanelManager UI rendering
|
||||
|
||||
### Property-Based Tests
|
||||
- **Property 1**: Generate random brand names, save and retrieve
|
||||
- **Property 2**: Generate brand name pairs differing only in case
|
||||
- **Property 3**: Generate brands, add twice with case variants
|
||||
- **Property 4**: Generate brand names with various whitespace patterns
|
||||
- **Property 5**: Generate brand names with mixed case, verify preservation
|
||||
- **Property 6**: Generate sets of brands, verify list count
|
||||
- **Property 7**: Test known brands and unknown brands for logo selection
|
||||
- **Property 8**: Render brand items, verify delete button presence
|
||||
- **Property 9**: Add and delete brands, verify complete removal
|
||||
- **Property 10**: Generate product card DOMs with brand info
|
||||
- **Property 11**: Generate product cards without brand info
|
||||
- **Property 12**: Generate products with blacklisted brands
|
||||
- **Property 13**: Add/remove brands, verify icon updates
|
||||
|
||||
### Integration Tests
|
||||
- End-to-end: Add brand → see icon on matching products → delete brand → icon removed
|
||||
- Menu navigation: Open menu → click Blacklist → verify panel content
|
||||
- Persistence: Add brands → reload page → verify brands persist
|
||||
|
||||
### Testing Framework
|
||||
- Jest für Unit Tests
|
||||
- fast-check für Property-Based Tests
|
||||
- JSDOM für DOM-Simulation
|
||||
|
||||
### Test Configuration
|
||||
- Minimum 100 Iterationen pro Property Test
|
||||
- Tag-Format: **Feature: blacklist-feature, Property {number}: {property_text}**
|
||||
- Jede Correctness Property wird durch einen einzelnen Property-Based Test implementiert
|
||||
98
.kiro/specs/blacklist-feature/requirements.md
Normal file
98
.kiro/specs/blacklist-feature/requirements.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
Eine Blacklist-Funktion für die Amazon Product Bar Extension, die es Nutzern ermöglicht, Markennamen zu verwalten und Produkte dieser Marken visuell zu kennzeichnen. Die Funktion wird als neuer Menüpunkt im bestehenden StaggeredMenu integriert und zeigt bei geblacklisteten Produkten ein entsprechendes Marken-Logo in der Product_Bar an.
|
||||
|
||||
## Glossary
|
||||
|
||||
- **Blacklist**: Liste von Markennamen, die der Nutzer als unerwünscht markiert hat
|
||||
- **Brand_Name**: Ein Markenname wie "Nike", "Adidas", "Puma" etc.
|
||||
- **Brand_Logo**: Visuelles Icon/Logo einer Marke, das in der Product_Bar angezeigt wird
|
||||
- **Product_Bar**: Die bestehende Leiste unter dem Produktbild auf Amazon-Suchergebnisseiten
|
||||
- **Blacklist_Panel**: Der Content-Bereich im Menü für die Blacklist-Verwaltung
|
||||
- **Case_Insensitive_Match**: Vergleich ohne Berücksichtigung von Groß-/Kleinschreibung
|
||||
- **Product_Brand**: Die Marke eines Produkts, extrahiert aus dem Produkttitel oder Produktdetails
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Blacklist-Menüpunkt
|
||||
|
||||
**User Story:** Als Nutzer möchte ich einen Blacklist-Menüpunkt im Menü haben, damit ich meine unerwünschten Marken verwalten kann.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN the menu is opened, THE Extension SHALL display a "Blacklist" menu item
|
||||
2. THE Blacklist menu item SHALL be positioned after the "Items" menu item
|
||||
3. WHEN the user clicks on "Blacklist", THE Extension SHALL display the Blacklist_Panel
|
||||
|
||||
### Requirement 2: Markennamen hinzufügen
|
||||
|
||||
**User Story:** Als Nutzer möchte ich Markennamen zur Blacklist hinzufügen können, damit ich unerwünschte Marken markieren kann.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN the Blacklist_Panel is open, THE Extension SHALL display an input field for brand names
|
||||
2. WHEN a user enters a brand name and confirms, THE Extension SHALL save the brand to the blacklist
|
||||
3. WHEN a brand name is saved, THE Extension SHALL store it in local storage
|
||||
4. WHEN a brand name is saved, THE Extension SHALL clear the input field
|
||||
5. IF a brand name already exists in the blacklist, THEN THE Extension SHALL display a message and prevent duplicate entry
|
||||
6. THE Extension SHALL trim whitespace from brand names before saving
|
||||
|
||||
### Requirement 3: Blacklist anzeigen
|
||||
|
||||
**User Story:** Als Nutzer möchte ich alle geblacklisteten Marken sehen können, damit ich einen Überblick habe.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN the Blacklist_Panel is open, THE Extension SHALL display all saved brand names in a list
|
||||
2. THE Extension SHALL display each brand name with its associated logo (if available)
|
||||
3. THE Extension SHALL provide a delete button for each blacklisted brand
|
||||
4. WHEN a brand is deleted, THE Extension SHALL remove it from storage and update the display
|
||||
|
||||
### Requirement 4: Case-Insensitive Matching
|
||||
|
||||
**User Story:** Als Nutzer möchte ich, dass die Groß-/Kleinschreibung bei der Markenerkennung egal ist, damit "Nike", "nike" und "NIKE" gleich behandelt werden.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN comparing brand names, THE Extension SHALL use case-insensitive comparison
|
||||
2. WHEN checking if a product matches a blacklisted brand, THE Extension SHALL ignore case differences
|
||||
3. WHEN checking for duplicate entries, THE Extension SHALL use case-insensitive comparison
|
||||
4. THE Extension SHALL preserve the original case when displaying brand names
|
||||
|
||||
### Requirement 5: Produkt-Marken-Erkennung
|
||||
|
||||
**User Story:** Als Nutzer möchte ich, dass die Extension automatisch erkennt, welche Marke ein Produkt hat, damit die Blacklist-Funktion funktioniert.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN a Product_Card is processed, THE Extension SHALL extract the brand name from the product
|
||||
2. THE Extension SHALL extract brand information from the product title
|
||||
3. THE Extension SHALL extract brand information from the "by [Brand]" text if available
|
||||
4. IF no brand can be extracted, THEN THE Extension SHALL not apply blacklist marking
|
||||
|
||||
### Requirement 6: Blacklist-Markierung in der Product_Bar
|
||||
|
||||
**User Story:** Als Nutzer möchte ich sehen, welche Produkte von geblacklisteten Marken sind, damit ich sie leicht erkennen kann.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN a product's brand matches a blacklisted brand, THE Product_Bar SHALL display a brand logo
|
||||
2. THE brand logo SHALL be displayed on the left side of the Product_Bar
|
||||
3. THE Extension SHALL use a generic "blocked" icon if no specific brand logo is available
|
||||
4. WHEN a brand is added to the blacklist, THE Extension SHALL immediately update all visible Product_Bars
|
||||
5. WHEN a brand is removed from the blacklist, THE Extension SHALL immediately remove the logo from matching Product_Bars
|
||||
|
||||
### Requirement 7: Marken-Logo-Verwaltung
|
||||
|
||||
**User Story:** Als Nutzer möchte ich, dass bekannte Marken mit ihrem Logo angezeigt werden, damit ich sie schnell erkennen kann.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Extension SHALL include a set of predefined brand logos for common brands
|
||||
2. THE predefined brands SHALL include at minimum: Nike, Adidas, Puma, Apple, Samsung
|
||||
3. WHEN a blacklisted brand has a predefined logo, THE Extension SHALL display that logo
|
||||
4. WHEN a blacklisted brand has no predefined logo, THE Extension SHALL display a generic blocked icon
|
||||
5. THE brand logos SHALL be displayed at a consistent size (16x16 pixels)
|
||||
|
||||
124
.kiro/specs/blacklist-feature/tasks.md
Normal file
124
.kiro/specs/blacklist-feature/tasks.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Implementation Plan: Blacklist Feature
|
||||
|
||||
## Overview
|
||||
|
||||
Implementierung der Blacklist-Funktion für die Amazon Product Bar Extension. Die Funktion ermöglicht das Verwalten von Markennamen und zeigt bei geblacklisteten Produkten ein Marken-Logo in der Product_Bar an.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [-] 1. Blacklist Storage Manager erstellen
|
||||
- [x] 1.1 Erstelle `src/BlacklistStorageManager.js` mit CRUD-Operationen
|
||||
- Implementiere `addBrand()`, `getBrands()`, `deleteBrand()`, `isBrandBlacklisted()`
|
||||
- Case-insensitive Duplikat-Check
|
||||
- Whitespace-Trimming vor dem Speichern
|
||||
- Event-Emission bei Änderungen
|
||||
- _Requirements: 2.2, 2.3, 2.5, 2.6, 4.1, 4.3_
|
||||
- [ ]* 1.2 Property Test: Brand Saving Round-Trip
|
||||
- **Property 1: Brand Saving Round-Trip**
|
||||
- **Validates: Requirements 2.2, 2.3**
|
||||
- [ ]* 1.3 Property Test: Case-Insensitive Comparison
|
||||
- **Property 2: Case-Insensitive Comparison**
|
||||
- **Validates: Requirements 4.1, 4.2**
|
||||
- [ ]* 1.4 Property Test: Duplicate Prevention
|
||||
- **Property 3: Duplicate Prevention**
|
||||
- **Validates: Requirements 2.5, 4.3**
|
||||
- [ ]* 1.5 Property Test: Whitespace Trimming
|
||||
- **Property 4: Whitespace Trimming**
|
||||
- **Validates: Requirements 2.6**
|
||||
- [ ]* 1.6 Property Test: Original Case Preservation
|
||||
- **Property 5: Original Case Preservation**
|
||||
- **Validates: Requirements 4.4**
|
||||
|
||||
- [x] 2. Brand Logo Registry erstellen
|
||||
- [x] 2.1 Erstelle `src/BrandLogoRegistry.js` mit vordefinierten Logos
|
||||
- SVG-Logos für Nike, Adidas, Puma, Apple, Samsung
|
||||
- Default "blocked" Icon
|
||||
- `getLogo()` und `hasLogo()` Methoden
|
||||
- _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5_
|
||||
- [ ]* 2.2 Property Test: Logo Selection Consistency
|
||||
- **Property 7: Logo Selection Consistency**
|
||||
- **Validates: Requirements 3.2, 6.3, 7.3, 7.4**
|
||||
|
||||
- [x] 3. Brand Extractor erstellen
|
||||
- [x] 3.1 Erstelle `src/BrandExtractor.js` für Markenextraktion
|
||||
- Extraktion aus "by [Brand]" Text
|
||||
- Extraktion aus Brand-Links
|
||||
- Fallback: Erstes Wort aus Produkttitel
|
||||
- _Requirements: 5.1, 5.2, 5.3, 5.4_
|
||||
- [ ]* 3.2 Property Test: Brand Extraction Determinism
|
||||
- **Property 10: Brand Extraction Determinism**
|
||||
- **Validates: Requirements 5.1, 5.2, 5.3**
|
||||
|
||||
- [x] 4. Checkpoint - Basis-Komponenten testen
|
||||
- Sicherstellen, dass alle Tests bestehen
|
||||
- Bei Fragen den Nutzer konsultieren
|
||||
|
||||
- [-] 5. Brand Icon Manager erstellen
|
||||
- [x] 5.1 Erstelle `src/BrandIconManager.js` für Icon-Verwaltung
|
||||
- `updateAllBars()` für initiales Laden
|
||||
- `addBrandIcon()` und `removeBrandIcon()` für einzelne Bars
|
||||
- `addIconForBrand()` und `removeIconForBrand()` für Marken-Updates
|
||||
- Integration mit BlacklistStorageManager, BrandExtractor, BrandLogoRegistry
|
||||
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
|
||||
- [ ]* 5.2 Property Test: Blacklist Icon Display
|
||||
- **Property 12: Blacklist Icon Display**
|
||||
- **Validates: Requirements 6.1**
|
||||
- [ ]* 5.3 Property Test: No Marking Without Brand
|
||||
- **Property 11: No Marking Without Brand**
|
||||
- **Validates: Requirements 5.4**
|
||||
|
||||
- [-] 6. Blacklist Panel Manager erstellen
|
||||
- [x] 6.1 Erstelle `src/BlacklistPanelManager.js` für UI-Verwaltung
|
||||
- `createBlacklistContent()` für Panel-Erstellung
|
||||
- Input-Feld und Add-Button
|
||||
- Brand-Liste mit Logos und Delete-Buttons
|
||||
- Feedback-Messages (success/error)
|
||||
- _Requirements: 2.1, 2.4, 3.1, 3.2, 3.3, 3.4_
|
||||
- [ ]* 6.2 Property Test: Brand List Rendering Completeness
|
||||
- **Property 6: Brand List Rendering Completeness**
|
||||
- **Validates: Requirements 3.1**
|
||||
- [ ]* 6.3 Property Test: Delete Button Presence
|
||||
- **Property 8: Delete Button Presence**
|
||||
- **Validates: Requirements 3.3**
|
||||
- [ ]* 6.4 Property Test: Deletion Completeness
|
||||
- **Property 9: Deletion Completeness**
|
||||
- **Validates: Requirements 3.4**
|
||||
|
||||
- [x] 7. CSS-Styles für Blacklist hinzufügen
|
||||
- [x] 7.1 Erweitere `src/StaggeredMenu.css` mit Blacklist-Styles
|
||||
- Blacklist Panel Styles (Header, Form, List)
|
||||
- Brand Item Styles (Logo, Name, Delete-Button)
|
||||
- Message Styles (success/error)
|
||||
- Brand Icon Styles für Product_Bar
|
||||
- _Requirements: 6.2, 7.5_
|
||||
|
||||
- [x] 8. StaggeredMenu Integration
|
||||
- [x] 8.1 Erweitere `src/StaggeredMenu.jsx` um Blacklist-Menüpunkt
|
||||
- Blacklist-Item nach Items-Item hinzufügen
|
||||
- BlacklistPanelManager importieren und initialisieren
|
||||
- Content-Panel für Blacklist rendern
|
||||
- _Requirements: 1.1, 1.2, 1.3_
|
||||
|
||||
- [x] 9. Content Script Integration
|
||||
- [x] 9.1 Erweitere `src/content.jsx` für Blacklist-Funktionalität
|
||||
- BrandIconManager initialisieren
|
||||
- Event-Listener für blacklist:updated
|
||||
- Icons bei Seitenlade aktualisieren
|
||||
- Real-time Updates bei Blacklist-Änderungen
|
||||
- _Requirements: 6.4, 6.5_
|
||||
- [ ]* 9.2 Property Test: Real-Time Icon Updates
|
||||
- **Property 13: Real-Time Icon Updates**
|
||||
- **Validates: Requirements 6.4, 6.5**
|
||||
|
||||
- [x] 10. Final Checkpoint
|
||||
- Sicherstellen, dass alle Tests bestehen
|
||||
- End-to-End Test: Marke hinzufügen → Icon erscheint → Marke löschen → Icon verschwindet
|
||||
- Bei Fragen den Nutzer konsultieren
|
||||
|
||||
## Notes
|
||||
|
||||
- Tasks mit `*` markiert sind optional und können für ein schnelleres MVP übersprungen werden
|
||||
- Jeder Task referenziert spezifische Requirements für Nachverfolgbarkeit
|
||||
- Checkpoints stellen inkrementelle Validierung sicher
|
||||
- Property Tests validieren universelle Korrektheitseigenschaften
|
||||
- Unit Tests validieren spezifische Beispiele und Edge Cases
|
||||
Reference in New Issue
Block a user