/* eslint-env jest */ import { analyze, scoreTone } from '../analyzer'; const base = { title: 'Zipper Hoodie: 5 Great Reasons to Love It | Shop', description: 'The zipper hoodie keeps you warm on cold days: heavy cotton, a two-way zipper and a hood that stays put. Free returns for thirty days, always.', slug: 'zipper-hoodie', content: '', keyword: 'zipper hoodie', hasFeaturedImage: true, siteHost: 'shop.example', }; const paragraph = (words: number, text = 'word'): string => `
${Array.from({ length: words }, () => text).join(' ')}
`; /** About 800 words with the keyword near 1%, links both ways, alt text and a list. */ const article = `The zipper hoodie is the warm layer for cold days. ${'Soft cotton keeps the cold out. '.repeat(12)}
A zipper hoodie opens fast. ${'The zipper hoodie stays put under a coat. '.repeat(6)}
More hoodies
Care guide
${paragraph(100, 'cosy')}
${paragraph(100, 'layer')}
${paragraph(100, 'zip')}
${paragraph(100, 'hood')}
${paragraph(100, 'cotton')}`;
describe('analyze', () => {
it('counts the words of a longer keyword once', () => {
const text = `${'The hoodie size chart helps. '.repeat(4)}${'Warm cotton layers. '.repeat(60)}
`; const one = analyze({ ...base, keyword: 'hoodie size', content: text }); const both = analyze({ ...base, keyword: 'hoodie size', keywords: ['hoodie size', 'hoodie'], content: text, }); const mentions = (analysis: typeof one): string => analysis.results.find(item => item.id === 'keywordDensity')?.message ?? ''; expect(mentions(one)).toContain('4 mentions'); expect(mentions(both)).toContain('4 mentions'); }); it('finds an accented keyword in the ASCII slug', () => { const { results } = analyze({ ...base, slug: 'ceainic-din-fonta', keyword: 'ceainic din fontă', }); expect(results.find(item => item.id === 'keywordInPermalink')?.passed).toBe( true ); expect( analyze({ ...base, slug: 'ceainic', keyword: 'ceainic din fontă', }).results.find(item => item.id === 'keywordInPermalink')?.passed ).toBe(false); }); it('weighs the post tests to 89 points, the informative ones to none', () => { const { results } = analyze({ ...base, content: article }); const total = results.reduce((sum, item) => sum + item.max, 0); expect(total).toBe(89); // A number in the title is not asked for. expect(results.map(item => item.id)).not.toContain('titleHasNumber'); expect(results.every(item => item.score <= item.max)).toBe(true); const informative = results .filter(item => item.max === 0 && item.group !== 'aeo') .map(item => item.id); expect(informative).toEqual([ 'linksHasExternals', 'linksNotAllExternals', 'titleSentiment', 'titleHasPowerWords', 'contentHasTOC', ]); }); it('shows the Rank Math tests the Recomaze rules do not ask for, with their note either way', () => { const { results } = analyze({ ...base, title: 'Zipper hoodie in heavy cotton', content: article, }); const rankMathOnly = results.filter(item => item.rankMathOnly); expect(rankMathOnly.map(item => item.id)).toEqual([ 'linksHasExternals', 'linksNotAllExternals', 'titleSentiment', 'titleHasPowerWords', ]); expect(rankMathOnly.every(item => item.max === 0)).toBe(true); expect(rankMathOnly.every(item => item.fix.includes('Recomaze'))).toBe( true ); // Advice is not a comparison: the AI answers keep asking for changes. expect( results.filter(item => item.group === 'aeo' && item.rankMathOnly) ).toHaveLength(0); }); it('applies the Recomaze rules: a density floor at 0.3% and products complete at 200 words', () => { // 2 mentions of a two-word keyword in 500 words = 0.4%: a mention // counts once, whatever the keyword's length. const thin = `The zipper hoodie. ${'word '.repeat(246)}A zipper hoodie. ${'word '.repeat(246)}
`; const density = analyze({ ...base, content: thin }).results.find( item => item.id === 'keywordDensity' ); expect(density?.passed).toBe(true); expect(density?.score).toBe(3); // 10 occurrences of a two-word keyword in 100 words = 20%. const stuffed = `${'The zipper hoodie is warm here. '.repeat(10)}${'word '.repeat(40)}
`; const high = analyze({ ...base, content: stuffed }).results.find( item => item.id === 'keywordDensity' ); expect(high?.passed).toBe(false); expect(high?.score).toBe(0); expect(high?.fix).toContain('too often'); const productCopy = `${'Zipper hoodie words. '.repeat(70)}
`; const product = analyze({ ...base, content: productCopy, postType: 'product', }).results.find(item => item.id === 'lengthContent'); expect(product?.score).toBe(8); const post = analyze({ ...base, content: productCopy }).results.find( item => item.id === 'lengthContent' ); expect(post?.passed).toBe(false); }); it('scores a well-optimised article high', () => { const analysis = analyze({ ...base, content: article }); const failed = analysis.results .filter(item => !item.passed) .map(item => item.id); expect(analysis.score).toBeGreaterThanOrEqual(81); expect(scoreTone(analysis.score)).toBe('great'); expect(failed).not.toContain('keywordInTitle'); expect(failed).not.toContain('keywordInPermalink'); expect(failed).not.toContain('keywordIn10Percent'); expect(failed).not.toContain('keywordInImageAlt'); expect(failed).not.toContain('linksHasInternal'); }); it('fails the keyword tests and asks for a keyword when none is set', () => { const analysis = analyze({ ...base, keyword: '', content: article }); expect(analysis.needsKeyword).toBe(true); expect( analysis.results.find(item => item.id === 'keywordInTitle')?.passed ).toBe(false); expect(analysis.results.some(item => item.id === 'keywordNotUsed')).toBe( false ); expect( analysis.results.filter(item => item.message === 'No focus keyword set.') ).toHaveLength(1); }); it('shows the keyword-used-before check only once known and never scores it', () => { const unknown = analyze({ ...base, content: article }); expect(unknown.results.some(item => item.id === 'keywordNotUsed')).toBe( false ); const used = analyze({ ...base, content: article, keywordUsedElsewhere: true, }); const check = used.results.find(item => item.id === 'keywordNotUsed'); expect(check?.passed).toBe(false); expect(check?.max).toBe(0); expect(used.score).toBe(unknown.score); }); it('runs only the archive tests for terms and normalises the score', () => { const term = analyze({ ...base, content: '', scope: 'term', description: 'All our zipper hoodie styles in one place.', }); expect(term.results).toHaveLength(7); expect(term.results.map(item => item.group)).not.toContain('content'); expect(term.results.reduce((sum, item) => sum + item.max, 0)).toBeLessThan( 100 ); expect(term.score).toBeGreaterThan(50); expect(term.score).toBeLessThanOrEqual(100); }); it('asks for a table of contents only from four H2 sections up', () => { const toc = (content: string) => analyze({ ...base, content }).results.find( item => item.id === 'contentHasTOC' ); const sections = ['Sizes', 'Care', 'Fit', 'Returns'] .map(heading => `The zipper hoodie is a cotton layer with a two-way zipper. It keeps the cold out.
' ).aeoAnswerFirst ).toBe(true); expect( passedById('Have you ever wondered which hoodie to buy?
') .aeoAnswerFirst ).toBe(false); expect( passedById('In this article we look at hoodies.
').aeoAnswerFirst ).toBe(false); expect(passedById(paragraph(80)).aeoAnswerFirst).toBe(false); }); it('judges the subheadings and the FAQ', () => { const pointed = 'Answer.
Answer.
Answer.
Answer.
Yes.
No.
Yes.
'; expect(passedById(faq).aeoFaq).toBe(true); expect( passedById('Answer.
Answer.
').aeoFaq).toBe(false); // Questions elsewhere in the post are no FAQ entries. expect( passedById( 'Answer.
Answer.
', }).results.find(item => item.id === 'aeoTable'); expect(compare?.passed).toBe(false); expect( analyze({ ...base, title: 'Zipper hoodie vs pullover', content: 'Answer.
| zip |
Answer.
').aeoTable).toBe(true); expect(passedById('Answer.
').aeoKeyTakeaways).toBe(true); expect(passedById(article).aeoKeyTakeaways).toBe(false); expect( passedById(`Punctele cheie:
${list}`)).toBe( true ); expect(takeaways(`Trois points.
${list}`)).toBe( true ); expect(takeaways(`The zipper hoodie is warm.
\n\nPe scurt, hanoracul cu fermoar ține de cald în diminețile reci și se spală ușor la treizeci de grade.
${list}` ) ).toBe(false); // A word inside another word is not the label. expect(takeaways(`The hoodie is warm. It costs less than a coat.
' ); expect(clean.aeoNoDashes).toBe(true); expect(clean.aeoNoBannedPhrases).toBe(true); expect(clean.aeoNoPricesOrStock).toBe(true); expect(passedById('Warm — and light.
').aeoNoDashes).toBe(false); const filler = analyze({ ...base, content: "In today's fast-paced world, this seamless hoodie is a game-changer; perhaps the best.
", }).results.find(item => item.id === 'aeoNoBannedPhrases'); expect(filler?.passed).toBe(false); expect(filler?.message).toContain("in today's"); expect( passedById('A cutting-edge, state-of-the-art hoodie.
') .aeoNoBannedPhrases ).toBe(false); expect( passedById('A handsome saying on the label.
').aeoNoBannedPhrases ).toBe(true); expect( passedById('Yours for $49.99, in stock now.
').aeoNoPricesOrStock ).toBe(false); expect( passedById('Only 3 left, order today.
').aeoNoPricesOrStock ).toBe(false); expect( passedById('Prices start from 120 lei.
').aeoNoPricesOrStock ).toBe(false); }); it('reads other languages, ranges and European prices, and leaves product pages their price', () => { expect( passedById('Store it at 10–20 °C, as we did from 2020–2024.
') .aeoNoDashes ).toBe(true); for (const price of ['89 €', 'EUR 89', '99 zł', '120 RON']) { expect( passedById(`It costs ${price} today.
`).aeoNoPricesOrStock ).toBe(false); } expect( analyze({ ...base, postType: 'product', content: 'In stock, 89 € with free delivery.
', }).results.find(item => item.id === 'aeoNoPricesOrStock')?.passed ).toBe(true); expect( passedById('Tricoul este în stoc, cu livrare în 24 de ore.
') .aeoNoPricesOrStock ).toBe(false); expect( passedById('Ultimele bucăți pleacă azi.
').aeoNoPricesOrStock ).toBe(false); expect( passedById('Stocul de bumbac organic crește.
').aeoNoPricesOrStock ).toBe(true); const opening = analyze({ ...base, content: 'Alegerea mărimii potrivite pentru un hanorac ține de trei măsurători.
', }).results.find(item => item.id === 'aeoAnswerFirst'); expect(opening?.passed).toBe(false); expect(opening?.message).toContain('starts with "Alegerea"'); expect( analyze({ ...base, content: 'În acest articol îți arătăm cum se măsoară.
', }).results.find(item => item.id === 'aeoAnswerFirst')?.message ).toContain('starts with "În acest articol"'); const faq = 'Răspuns.
Da.
Nu.
Da.
'; expect(passedById(faq).aeoFaq).toBe(true); expect( passedById( 'Răspuns.
Answer.
' }).results.find( item => item.id === 'aeoTable' )?.passed; expect(table('Which size should I pick?')).toBe(true); expect(table('Best way to clean leather')).toBe(true); expect(table('Best cast iron skillets for beginners')).toBe(false); expect(table('Top 5 hoodies of the year')).toBe(false); }); }); it('matches the keyword as whole words, without accents, in content with or without paragraph tags', () => { const inside = analyze({ ...base, keyword: 'cat', title: 'Categories and education', content: 'Categories of education.
', }).results; expect(inside.find(item => item.id === 'keywordInTitle')?.passed).toBe( false ); expect(inside.find(item => item.id === 'keywordDensity')?.passed).toBe( false ); const romanian = analyze({ ...base, keyword: 'cizme de drumetie', title: 'Cizme de drumeție impermeabile', slug: 'cizme-de-drumetie', content: 'Cizmele de drumeţie ţin picioarele uscate. Cizme de drumeție pentru munte.
', }).results; expect(romanian.find(item => item.id === 'keywordInTitle')?.passed).toBe( true ); expect(romanian.find(item => item.id === 'keywordInContent')?.passed).toBe( true ); const german = analyze({ ...base, keyword: 'Größe wählen', slug: 'groesse-waehlen', }).results; expect(german.find(item => item.id === 'keywordInPermalink')?.passed).toBe( true ); const plain = analyze({ ...base, content: `The zipper hoodie keeps you warm.\n\n${'Another short paragraph here. '.repeat(10)}`, }).results.find(item => item.id === 'contentHasShortParagraphs'); expect(plain?.passed).toBe(true); expect(plain?.message).not.toContain('0 words'); }); it('reads a Romanian page by its own words, whatever the site says: the keyword in any form', () => { const results = analyze({ ...base, keyword: 'hanorac', title: 'Cum alegi hanoracul potrivit: ghid de mărimi', description: 'Alegerea mărimii potrivite pentru un hanorac ține de trei măsurători.', slug: 'ghid-marimi-hanorac', content: 'Alegerea mărimii potrivite pentru hanoracul tău ține de trei măsurători.
Ai nevoie de un centimetru.
', siteLanguage: 'en_US', }).results; [ 'keywordInTitle', 'keywordInMetaDescription', 'keywordInPermalink', 'keywordIn10Percent', 'keywordInContent', 'keywordInSubheadings', 'titleStartWithKeyword', ].forEach(id => expect([id, results.find(item => item.id === id)?.passed]).toEqual([ id, true, ]) ); expect( results.find(item => item.id === 'keywordDensity')?.message ).toContain('2 mentions'); }); it('finds the words of a keyword in any order, but only within one sentence', () => { const skillet = { ...base, keyword: '26 cm cast iron skillet', title: 'Cast iron skillet 26 cm: the pan for every hob', slug: 'cast-iron-skillet-26-cm', }; const found = analyze({ ...skillet, content: 'This cast iron skillet 26 cm heats evenly.
', }).results; ['keywordInTitle', 'keywordInPermalink', 'keywordInContent'].forEach(id => expect([id, found.find(item => item.id === id)?.passed]).toEqual([ id, true, ]) ); const apart = analyze({ ...skillet, content: 'This skillet is cast iron. It measures 26 cm.
', }).results; expect(apart.find(item => item.id === 'keywordInContent')?.passed).toBe( false ); }); it('counts density over every focus keyword, one mention each, and says so', () => { const text = `${'Zipper hoodie for winter. Fleece jacket too. '.repeat(10)}${'word '.repeat(1000)}
`; const primary = analyze({ ...base, content: text }).results.find( item => item.id === 'keywordDensity' ); expect(primary?.message).toContain('10 mentions in 1070 words'); expect(primary?.score).toBe(3); const both = analyze({ ...base, content: text, keywords: ['zipper hoodie', 'fleece jacket'], }).results.find(item => item.id === 'keywordDensity'); expect(both?.message).toContain('20 mentions'); expect(both?.score).toBe(6); }); it('reads the beginning of a short text whole and the title by its first half', () => { const short = `${'word '.repeat(60)}The zipper hoodie closes the text.
`; expect( analyze({ ...base, content: short }).results.find( item => item.id === 'keywordIn10Percent' )?.passed ).toBe(true); const title = analyze({ ...base, title: 'Warm zipper hoodie for cold mornings and long evenings', }).results.find(item => item.id === 'titleStartWithKeyword'); expect(title?.passed).toBe(true); }); it('finds the keyword in the featured image alt and in alt texts with an apostrophe', () => { const results = analyze({ ...base, keyword: "men's hiking boots", content: 'Text.
',
}).results;
expect(results.find(item => item.id === 'keywordInImageAlt')?.passed).toBe(
true
);
const featured = analyze({
...base,
content: 'Text.
', imageAlts: ['A zipper hoodie in grey'], }).results.find(item => item.id === 'keywordInImageAlt'); expect(featured?.passed).toBe(true); }); it('marks a test that earned part of its points and keeps its advice', () => { // 2 mentions of a two-word keyword in 500 words: 0.4%, above the floor. const thin = `The zipper hoodie. ${'word '.repeat(246)}A zipper hoodie. ${'word '.repeat(246)}
`; const density = analyze({ ...base, content: thin }).results.find( item => item.id === 'keywordDensity' ); expect(density?.passed).toBe(true); expect(density?.partial).toBe(true); expect(density?.score).toBe(3); expect(density?.fix).toContain('a few more times'); }); it('passes an article from 600 words on Rank Math points, and asks a product for 200', () => { const length = (words: number, postType?: string) => analyze({ ...base, postType, content: `${'word '.repeat(words)}
`, }).results.find(item => item.id === 'lengthContent'); expect(length(599)).toMatchObject({ passed: false, score: 0 }); expect(length(599)?.fix).toContain('at least 600 words'); // The pilot's article: four of eight points, and nothing left to do. for (const [words, points] of [ [600, 2], [1100, 3], [1855, 4], [2000, 5], [2500, 8], ]) { expect(length(words)).toMatchObject({ passed: true, partial: false, score: points, fix: '', }); } expect(length(150, 'product')).toMatchObject({ passed: true, partial: true, score: 4, }); expect(length(150, 'product')?.fix).toContain('at least 200 words'); expect(length(200, 'product')).toMatchObject({ partial: false, score: 8 }); }); it('judges a secondary keyword on the text and a subheading only', () => { const content = `Our zipper hoodie keeps you warm on winter walks.
A fleece lining helps on cold mornings.
`; const secondary = (keyword: string, scope?: 'term') => analyze({ ...base, content, keyword, role: 'secondary', scope }); const both = secondary('cold morning'); expect(both.results.map(item => item.id)).toEqual([ 'keywordInContent', 'keywordInSubheadings', ]); expect(both.score).toBe(100); // In the text, in no subheading: half the points. expect(secondary('fleece lining').score).toBe(50); expect(secondary('wool socks').score).toBe(0); // An archive's secondary keyword: its description. const term = secondary('cold days', 'term'); expect(term.results.map(item => item.id)).toEqual([ 'keywordInMetaDescription', ]); expect(term.score).toBe(100); // The first keyword keeps every test. expect( analyze({ ...base, content, keyword: 'cold morning' }).results.length ).toBeGreaterThan(10); }); }); describe('the product listing group', () => { const bare = { price: '', image: false, gallery: 0, shortDescription: '', gtin: '', mpn: '', brand: '', categories: 0, reviews: 0, }; const ids = (input: Parameters