yoastseo-dep
Version:
Yoast clientside page analysis
1,389 lines (1,143 loc) • 75.3 kB
JavaScript
// External dependencies
import { forEach, isArray, isNumber, isObject } from "lodash-es";
import { getLogger } from "loglevel";
// Internal dependencies
import AnalysisWebWorker from "../../src/worker/AnalysisWebWorker";
import { createShortlink } from "../../src/helpers/shortlinker";
import Assessment from "../../src/scoring/assessments/assessment";
import SEOAssessor from "../../src/scoring/seoAssessor";
import contentAssessor from "../../src/scoring/contentAssessor";
import { SEOScoreAggregator } from "../../src/parsedPaper/assess/scoreAggregators";
import { TreeResearcher } from "../../src/parsedPaper/research";
import AssessmentResult from "../../src/values/AssessmentResult";
import Paper from "../../src/values/Paper";
import InvalidTypeError from "../../src/errors/invalidType";
import { StructuredNode } from "../../src/parsedPaper/structure/tree";
// Full-length texts to test
import testTexts from "../fullTextTests/testTexts";
// Test helpers
import TestResearch from "../specHelpers/tree/TestResearch";
import getMorphologyData from "../specHelpers/getMorphologyData";
import TestAssessment from "../specHelpers/tree/TestAssessment";
import EnglishResearcher from "../../src/languageProcessing/languages/en/Researcher";
let researcher = new EnglishResearcher();
const morphologyData = getMorphologyData( "en" );
/**
* Creates a mocked scope.
*
* @returns {Object} The mocked scope.
*/
function createScope() {
return {
postMessage: jest.fn(),
importScripts: jest.fn(),
};
}
/**
* Creates a message object.
*
* @param {string} type The message type.
* @param {Object} [payload={}] The payload.
* @param {number} [id=0] The request id.
*
* @returns {Object} The message.
*/
function createMessage( type, payload = {}, id = 0 ) {
return {
data: {
type,
id,
payload,
},
};
}
/**
* Creates an assessment.
*
* @param {string} name The assessment identifier.
* @param {number} score The result score.
* @param {string} text The result text.
*
* @returns {Assessment} The assessment.
*/
function createAssessment( name, score = 9, text = "Excellent" ) {
const assessment = new Assessment();
assessment.identifier = name;
assessment.getResult = () => {
const result = new AssessmentResult();
result.setScore( score );
result.setText( text );
return result;
};
return assessment;
}
// Re-using these global variables.
let scope = null;
let worker = null;
/*
* Couple of things to note:
* - Transporter is used to serialize and parse the payload. However,
* without the wrapper we need to pass serialized data in the message.
* - A task is async. Using the send function as the test trigger.
* - Initialize needs to get called first most of the time.
*/
describe( "AnalysisWebWorker", () => {
afterEach( () => {
// Make sure we don't keep polling after the tests are done.
if ( worker ) {
worker._scheduler.stopPolling();
}
} );
describe( "constructor", () => {
test( "initializes without errors", () => {
scope = createScope();
worker = null;
try {
worker = new AnalysisWebWorker( scope, researcher );
} catch ( error ) {
// eslint-ignore-line no-empty
}
expect( worker ).not.toBeNull();
expect( worker._scope ).toBe( scope );
} );
} );
describe( "register", () => {
beforeEach( () => {
scope = createScope();
worker = new AnalysisWebWorker( scope, researcher );
} );
test( "binds onmessage", () => {
expect( scope.onmessage ).not.toBeDefined();
worker.register();
expect( scope.onmessage ).toBeDefined();
} );
test( "listens to messages", () => {
worker.handleMessage = jest.fn();
worker.register();
const message = createMessage( "test" );
scope.onmessage( message );
expect( worker.handleMessage ).toHaveBeenCalledTimes( 1 );
expect( worker.handleMessage ).toBeCalledWith( message );
} );
test( "provides globals", () => {
expect( scope.analysisWorker ).not.toBeDefined();
expect( scope.yoast ).not.toBeDefined();
worker.register();
expect( scope.analysisWorker ).toBeDefined();
} );
} );
describe( "handleMessage", () => {
describe( "console", () => {
beforeEach( () => {
scope = createScope();
worker = new AnalysisWebWorker( scope, researcher );
worker.register();
} );
test( "falls back to a warning", () => {
console.warn = jest.fn();
scope.onmessage( createMessage( "non-existing message type" ) );
expect( console.warn ).toHaveBeenCalledTimes( 1 );
expect( console.warn ).toHaveBeenCalledWith( "AnalysisWebWorker unrecognized action:", "non-existing message type" );
} );
test( "calls logger debug", () => {
const logger = getLogger( "yoast-analysis-worker" );
const spy = spyOn( logger, "debug" );
scope.onmessage( createMessage( "initialize" ) );
expect( spy ).toHaveBeenCalledTimes( 2 );
expect( spy ).toHaveBeenCalledWith( "AnalysisWebWorker incoming:", "initialize", 0, {} );
expect( spy ).toHaveBeenCalledWith( "AnalysisWebWorker outgoing:", "initialize:done", 0, {} );
} );
} );
describe( "shouldAssessorsUpdate", () => {
const updateAll = { readability: true, seo: true, inclusiveLanguage: true };
const updateNone = { readability: false, seo: false, inclusiveLanguage: false };
const updateReadability = { readability: true, seo: false, inclusiveLanguage: false };
const updateSEO = { readability: false, seo: true, inclusiveLanguage: false };
const updateSEOAndReadability = { readability: true, seo: true, inclusiveLanguage: false };
test( "update all when an empty configuration is passed", () => {
expect( AnalysisWebWorker.shouldAssessorsUpdate( {} ) ).toEqual( updateAll );
} );
test( "update all when an empty configuration is passed along with null assessors", () => {
expect( AnalysisWebWorker.shouldAssessorsUpdate( {}, null, null ) ).toEqual( updateAll );
} );
test( "update none when an empty configuration is passed along with non-null assessors", () => {
expect( AnalysisWebWorker.shouldAssessorsUpdate( {}, false, false, false ) ).toEqual( updateNone );
} );
test( "update readability with contentAnalysisActive", () => {
expect( AnalysisWebWorker.shouldAssessorsUpdate( { contentAnalysisActive: true }, false, false, false ) )
.toEqual( updateReadability );
} );
test( "update seo with keywordAnalysisActive", () => {
expect( AnalysisWebWorker.shouldAssessorsUpdate( { keywordAnalysisActive: true }, false, false, false ) )
.toEqual( updateSEO );
} );
test( "update both SEO and readability with useCornerstone", () => {
expect( AnalysisWebWorker.shouldAssessorsUpdate( { useCornerstone: true }, false, false, false ) )
.toEqual( updateSEOAndReadability );
} );
test( "update seo with useTaxonomy", () => {
expect( AnalysisWebWorker.shouldAssessorsUpdate( { useTaxonomy: true }, false, false, false ) ).toEqual( updateSEO );
} );
test( "update all with locale", () => {
expect( AnalysisWebWorker.shouldAssessorsUpdate( { locale: "en_US" }, false, false, false ) ).toEqual( updateAll );
} );
test( "update all with translations", () => {
expect( AnalysisWebWorker.shouldAssessorsUpdate( { translations: {} }, false, false, false ) ).toEqual( updateAll );
} );
test( "update seo with researchData", () => {
expect( AnalysisWebWorker.shouldAssessorsUpdate( { researchData: {} }, false, false, false ) ).toEqual( updateSEO );
} );
test( "update both SEO and readability with customAnalysis", () => {
expect( AnalysisWebWorker.shouldAssessorsUpdate( { customAnalysisType: "test" }, false, false, false ) )
.toEqual( updateSEOAndReadability );
} );
} );
describe( "initialize", () => {
beforeEach( () => {
scope = createScope();
worker = new AnalysisWebWorker( scope, researcher );
worker.register();
} );
test( "calls initialize", () => {
const configuration = { testing: true };
worker.initialize = jest.fn();
scope.onmessage( createMessage( "initialize", { configuration } ) );
expect( worker.initialize ).toHaveBeenCalledTimes( 1 );
expect( worker.initialize ).toHaveBeenCalledWith( 0, { configuration } );
} );
test( "updates the configuration", () => {
scope.onmessage( createMessage( "initialize", { testing: true } ) );
expect( worker._configuration ).toBeDefined();
expect( worker._configuration.testing ).toBe( true );
} );
test( "overwrites default configuration", () => {
expect( worker._configuration.contentAnalysisActive ).toBe( true );
scope.onmessage( createMessage( "initialize", { contentAnalysisActive: false } ) );
expect( worker._configuration.contentAnalysisActive ).toBe( false );
} );
test( "creates the i18n", () => {
scope.onmessage( createMessage( "initialize", {
messages: {
domain: "messages",
// eslint-disable-next-line camelcase
locale_data: {
messages: {
"": {},
test: [ "1234" ],
},
},
},
} ) );
} );
test( "sets the locale", () => {
expect( worker._configuration.locale ).toBe( "en_US" );
worker.createContentAssessor = jest.fn();
scope.onmessage( createMessage( "initialize", { locale: "nl_NL" } ) );
expect( worker._configuration.locale ).toBe( "nl_NL" );
expect( worker.createContentAssessor ).toHaveBeenCalledTimes( 1 );
} );
test( "sets the log level", () => {
const logger = getLogger( "yoast-analysis-worker" );
const saveLogLevel = logger.getLevel();
const levels = {
TRACE: 0,
DEBUG: 1,
INFO: 2,
WARN: 3,
ERROR: 4,
};
// Disable actual logging in the tests.
/* eslint-disable-next-line no-console */
console.log = jest.fn();
forEach( levels, ( expected, name ) => {
scope.onmessage( createMessage( "initialize", { logLevel: name } ) );
expect( logger.getLevel() ).toBe( expected );
} );
logger.setLevel( saveLogLevel, false );
} );
test( "adds the research data to the researcher", () => {
worker._researcher.addResearchData = jest.fn();
scope.onmessage( createMessage( "initialize", {
researchData: {
morphology: "word forms",
fancy: "feature",
},
} ) );
expect( worker._researcher.addResearchData ).toHaveBeenNthCalledWith( 1, "morphology", "word forms" );
expect( worker._researcher.addResearchData ).toHaveBeenNthCalledWith( 2, "fancy", "feature" );
} );
test( "configures the shortlinker params", () => {
const baseUrl = "https://yoast.com";
// Ensure there are no params registered yet.
expect( createShortlink( baseUrl ) ).toBe( baseUrl );
scope.onmessage( createMessage( "initialize", {
defaultQueryParams: {
source: "specs",
},
} ) );
expect( createShortlink( baseUrl ) ).toBe( `${ baseUrl }?source=specs` );
} );
test( "creates the assessors", () => {
worker.createContentAssessor = jest.fn();
worker.createSEOAssessor = jest.fn();
scope.onmessage( createMessage( "initialize", {} ) );
expect( worker.createContentAssessor ).toHaveBeenCalledTimes( 1 );
expect( worker.createSEOAssessor ).toHaveBeenCalledTimes( 1 );
} );
test( "clears the cache", () => {
worker.clearCache = jest.fn();
scope.onmessage( createMessage( "initialize", {} ) );
expect( worker.clearCache ).toHaveBeenCalledTimes( 1 );
} );
test( "sends the done message", () => {
scope.onmessage( createMessage( "initialize" ) );
expect( scope.postMessage ).toHaveBeenCalledTimes( 1 );
expect( scope.postMessage ).toBeCalledWith( createMessage( "initialize:done" ).data );
} );
test( "starts the polling of the scheduler", () => {
worker._scheduler.startPolling = jest.fn();
scope.onmessage( createMessage( "initialize" ) );
expect( worker._scheduler.startPolling ).toHaveBeenCalledTimes( 1 );
} );
test( "updates readability assessor", () => {
let timesCalled = 0;
worker.createContentAssessor = jest.fn().mockReturnValue( false );
// When initializing.
scope.onmessage( createMessage( "initialize", {} ) );
expect( worker.createContentAssessor ).toHaveBeenCalledTimes( ++timesCalled );
// When switching readability analysis on/off.
scope.onmessage( createMessage( "initialize", { contentAnalysisActive: true } ) );
expect( worker.createContentAssessor ).toHaveBeenCalledTimes( ++timesCalled );
// Not when switching seo analysis on/off.
scope.onmessage( createMessage( "initialize", { keywordAnalysisActive: true } ) );
expect( worker.createContentAssessor ).toHaveBeenCalledTimes( timesCalled );
// When switching cornerstone content on/off.
scope.onmessage( createMessage( "initialize", { useCornerstone: true } ) );
expect( worker.createContentAssessor ).toHaveBeenCalledTimes( ++timesCalled );
// Not when switching taxonomy assessor on/off.
scope.onmessage( createMessage( "initialize", { useTaxonomy: true } ) );
expect( worker.createContentAssessor ).toHaveBeenCalledTimes( timesCalled );
// When changing locale.
scope.onmessage( createMessage( "initialize", { locale: "en_US" } ) );
expect( worker.createContentAssessor ).toHaveBeenCalledTimes( ++timesCalled );
// When changing translations.
scope.onmessage( createMessage( "initialize", { translations: {} } ) );
expect( worker.createContentAssessor ).toHaveBeenCalledTimes( ++timesCalled );
} );
test( "updates seo assessor", () => {
let timesCalled = 0;
worker.createSEOAssessor = jest.fn().mockReturnValue( false );
// When initializing.
scope.onmessage( createMessage( "initialize", {} ) );
expect( worker.createSEOAssessor ).toHaveBeenCalledTimes( ++timesCalled );
// Not when switching readability analysis on/off.
scope.onmessage( createMessage( "initialize", { contentAnalysisActive: true } ) );
expect( worker.createSEOAssessor ).toHaveBeenCalledTimes( timesCalled );
// When switching seo analysis on/off.
scope.onmessage( createMessage( "initialize", { keywordAnalysisActive: true } ) );
expect( worker.createSEOAssessor ).toHaveBeenCalledTimes( ++timesCalled );
// When switching cornerstone content on/off.
scope.onmessage( createMessage( "initialize", { useCornerstone: true } ) );
expect( worker.createSEOAssessor ).toHaveBeenCalledTimes( ++timesCalled );
// When switching taxonomy assessor on/off.
scope.onmessage( createMessage( "initialize", { useTaxonomy: true } ) );
expect( worker.createSEOAssessor ).toHaveBeenCalledTimes( ++timesCalled );
// When changing locale.
scope.onmessage( createMessage( "initialize", { locale: "en_US" } ) );
expect( worker.createSEOAssessor ).toHaveBeenCalledTimes( ++timesCalled );
// When changing translations.
scope.onmessage( createMessage( "initialize", { translations: {} } ) );
expect( worker.createSEOAssessor ).toHaveBeenCalledTimes( ++timesCalled );
} );
} );
describe( "analyze", () => {
beforeEach( () => {
scope = createScope();
worker = new AnalysisWebWorker( scope, researcher );
worker.register();
} );
test( "schedules a task", () => {
const paper = new Paper( "This is the content." );
worker._scheduler.schedule = jest.fn();
scope.onmessage( createMessage( "analyze", { paper: paper.serialize() } ) );
expect( worker._scheduler.schedule ).toHaveBeenCalledTimes( 1 );
expect( worker._scheduler.schedule ).toHaveBeenCalledWith( {
id: 0,
execute: worker.analyze,
done: worker.analyzeDone,
data: { paper },
type: "analyze",
} );
} );
test( "calls analyze", done => {
const paper = new Paper( "This is the content." );
const spy = spyOn( worker, "analyze" );
worker.analyzeDone = () => {
expect( spy ).toHaveBeenCalledTimes( 1 );
expect( spy ).toHaveBeenCalledWith( 0, { paper } );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "analyze", { paper: paper.serialize() } ) );
} );
test( "returns results", done => {
const paper = testTexts[ 0 ].paper;
worker.analyzeDone = ( id, result ) => {
expect( id ).toBe( 0 );
expect( isObject( result ) ).toBe( true );
expect( isObject( result.readability ) ).toBe( true );
expect( isArray( result.readability.results ) ).toBe( true );
expect( isNumber( result.readability.score ) ).toBe( true );
expect( isObject( result.seo ) ).toBe( true );
expect( isObject( result.seo[ "" ] ) ).toBe( true );
expect( isArray( result.seo[ "" ].results ) ).toBe( true );
expect( isNumber( result.seo[ "" ].score ) ).toBe( true );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "analyze", { paper: paper.serialize() } ) );
} );
// Skipped because the input isn't valid HTML -- editors will generally take care of that.
it.skip( "does not assess the tree when it could not be built", done => {
const paper = new Paper( "<h1>This </ fails." );
worker.analyzeDone = ( id, result ) => {
expect( id ).toBe( 0 );
expect( isObject( result ) ).toBe( true );
expect( isObject( result.readability ) ).toBe( true );
expect( isArray( result.readability.results ) ).toBe( true );
expect( isNumber( result.readability.score ) ).toBe( true );
expect( isObject( result.seo ) ).toBe( true );
expect( isObject( result.seo[ "" ] ) ).toBe( true );
expect( isArray( result.seo[ "" ].results ) ).toBe( true );
expect( isNumber( result.seo[ "" ].score ) ).toBe( true );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "analyze", { paper: paper.serialize() } ) );
} );
test( "skips over researcher set paper and locale when there are no paper changes", done => {
const paper = new Paper( "This is the content." );
// Using setLocale because setPaper is also used in the researcher. This makes is simpler.
worker.setLocale = jest.fn();
let firstRun = true;
worker.analyzeDone = () => {
expect( worker.setLocale ).toHaveBeenCalledTimes( 1 );
if ( firstRun ) {
scope.onmessage( createMessage( "analyze", { paper: paper.serialize() } ) );
firstRun = false;
return;
}
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "analyze", { paper: paper.serialize() } ) );
} );
test( "listens to the contentAnalysisActive configuration", done => {
const paper = testTexts[ 0 ].paper;
worker.analyzeDone = ( id, result ) => {
// Results still get initialized.
expect( result.readability.results.length ).toBe( 0 );
expect( result.readability.score ).toBe( 0 );
done();
};
scope.onmessage( createMessage( "initialize", { contentAnalysisActive: false } ) );
scope.onmessage( createMessage( "analyze", { paper: paper.serialize() } ) );
} );
test( "listens to the keywordAnalysisActive configuration", done => {
const paper = testTexts[ 0 ].paper;
worker.analyzeDone = ( id, result ) => {
// Results still get initialized.
expect( result.seo[ "" ].results.length ).toBe( 0 );
expect( result.seo[ "" ].score ).toBe( 0 );
done();
};
scope.onmessage( createMessage( "initialize", { keywordAnalysisActive: false } ) );
scope.onmessage( createMessage( "analyze", { paper: paper.serialize() } ) );
} );
test( "processes related keywords", done => {
const paper = testTexts[ 0 ].paper;
worker.analyzeDone = ( id, result ) => {
expect( isObject( result.seo.a ) ).toBe( true );
expect( isArray( result.seo.a.results ) ).toBe( true );
expect( isNumber( result.seo.a.score ) ).toBe( true );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "analyze", {
paper: paper.serialize(),
relatedKeywords: {
a: { keyword: "technology" },
b: { keyword: "tech" },
},
} ) );
} );
test( "analyze done calls send", () => {
worker.send = jest.fn();
worker.analyzeDone( 0, { result: true } );
expect( worker.send ).toHaveBeenCalledTimes( 1 );
expect( worker.send ).toHaveBeenCalledWith( "analyze:done", 0, { result: true } );
} );
test( "handles analyze error", done => {
const paper = new Paper( "This is the content." );
// Mock the first function call in analyze to throw an error.
worker.shouldReadabilityUpdate = () => {
throw new Error( "Simulated error!" );
};
worker.analyzeDone = ( id, result ) => {
expect( id ).toBe( 0 );
expect( isObject( result ) ).toBe( true );
expect( result.error ).toBeDefined();
expect( result.error ).toBe( "An error occurred while running the analysis.\n\tError: Simulated error!" );
done();
};
// Silent to prevent console logging in the tests.
scope.onmessage( createMessage( "initialize", { logLevel: "silent" } ) );
scope.onmessage( createMessage( "analyze", { paper: paper.serialize() } ) );
} );
test( "handles analyze error, with stack trace", done => {
const paper = new Paper( "This is the content." );
// Mock the console to see if it is used and to not output anything for real.
// eslint-disable-next-line no-console
console.log = jest.fn();
// eslint-disable-next-line no-console
console.error = jest.fn();
// Mock the first function call in analyze to throw an error.
worker.shouldReadabilityUpdate = () => {
throw new Error( "Simulated error!" );
};
worker.analyzeDone = ( id, result ) => {
expect( id ).toBe( 0 );
expect( isObject( result ) ).toBe( true );
expect( result.error ).toBeDefined();
expect( result.error ).toBe( "An error occurred while running the analysis.\n\tError: Simulated error!" );
// eslint-disable-next-line no-console
expect( console.log ).toHaveBeenCalled();
// eslint-disable-next-line no-console
expect( console.error ).toHaveBeenCalled();
done();
};
scope.onmessage( createMessage( "initialize", { logLevel: "trace" } ) );
scope.onmessage( createMessage( "analyze", { paper: paper.serialize() } ) );
} );
test( "analyze done calls send on failure", () => {
worker.send = jest.fn();
worker.analyzeDone( 0, { error: "failed" } );
expect( worker.send ).toHaveBeenCalledTimes( 1 );
expect( worker.send ).toHaveBeenCalledWith( "analyze:failed", 0, { error: "failed" } );
} );
it( "correctly calculates sentence position in a node containing an element (comment) that is removed from" +
"the paper after building the tree", async() => {
// One paragraph, with one sentence.
const html = "<div><!-- A comment --><p>A paragraph</p></div>";
const paper = new Paper( html );
const webworker = new AnalysisWebWorker( scope, researcher );
await webworker.analyze( 1, { paper } );
// Get the sentence from the single paragraph in the tree.
const paragraphs = paper.getTree().findAll( node => node.name === "p" );
const sentence = paragraphs[ 0 ].sentences[ 0 ];
const { startOffset, endOffset } = sentence.sourceCodeRange;
// Check if the source code position is correct.
expect( html.slice( startOffset, endOffset ) ).toEqual( "A paragraph" );
} );
it( "correctly calculate the position of the image with a caption", async() => {
const html = "<!-- wp:image -->\n" +
"<figure class=\"wp-block-image size-large\"><img src=\"https://example.com\" alt=\"\" class=\"wp-image-8\"/>" +
"<figcaption class=\"wp-element-caption\">A cute cat</figcaption></figure>\n" +
"<!-- /wp:image -->\n" +
"<!-- wp:paragraph -->\n" +
"<p>Movet voluptatibus vix ad. Et eruditi mediocrem liberavisse eos.</p>" +
"<!-- /wp:paragraph -->";
const paper = new Paper( html );
const webworker = new AnalysisWebWorker( scope, researcher );
await webworker.analyze( 1, { paper } );
const tree = paper.getTree();
const images = tree.findAll( node => node.name === "img" );
const caption = tree.findAll( node => node.name === "figcaption" );
const captionText = caption[ 0 ].findAll( node => node.name === "p" );
const { startOffset, endOffset } = captionText[ 0 ].sentences[ 0 ].sourceCodeRange;
// Check if the source code position is correct.
expect( images[ 0 ].sourceCodeLocation ).toEqual( {
endOffset: 118,
startOffset: 60,
startTag: { endOffset: 118, startOffset: 60 },
} );
// Check if the startOffset and endOffset of the caption text is correct.
expect( startOffset ).toEqual( 157 );
expect( endOffset ).toEqual( 167 );
} );
} );
describe( "analyzeRelatedKeywords", () => {
beforeEach( () => {
scope = createScope();
worker = new AnalysisWebWorker( scope, researcher );
worker.register();
} );
test( "schedules a task", () => {
const paper = new Paper( "This is the content." );
const relatedKeywords = { a: { keyword: "content", synonyms: "" } };
worker._scheduler.schedule = jest.fn();
scope.onmessage( createMessage( "analyzeRelatedKeywords", {
paper: paper.serialize(),
relatedKeywords,
} ) );
expect( worker._scheduler.schedule ).toHaveBeenCalledTimes( 1 );
expect( worker._scheduler.schedule ).toHaveBeenCalledWith( {
id: 0,
execute: worker.analyzeRelatedKeywords,
done: worker.analyzeRelatedKeywordsDone,
data: { paper, relatedKeywords },
type: "analyzeRelatedKeywords",
} );
} );
test( "calls analyzeRelatedKeywords", done => {
const paper = new Paper( "This is the content." );
const relatedKeywords = { a: { keyword: "content", synonyms: "" } };
const spy = spyOn( worker, "analyzeRelatedKeywords" );
worker.analyzeRelatedKeywordsDone = () => {
expect( spy ).toHaveBeenCalledTimes( 1 );
expect( spy ).toHaveBeenCalledWith( 0, { paper, relatedKeywords } );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "analyzeRelatedKeywords", { paper: paper.serialize(), relatedKeywords } ) );
} );
test( "returns results", done => {
const paper = testTexts[ 0 ].paper;
const relatedKeywords = { a: { keyword: "content", synonyms: "" } };
worker.analyzeRelatedKeywordsDone = ( id, result ) => {
expect( id ).toBe( 0 );
expect( isObject( result ) ).toBe( true );
expect( isObject( result.readability ) ).toBe( true );
expect( isArray( result.readability.results ) ).toBe( true );
expect( isNumber( result.readability.score ) ).toBe( true );
expect( isObject( result.seo ) ).toBe( true );
expect( isObject( result.seo[ "" ] ) ).toBe( true );
expect( isArray( result.seo[ "" ].results ) ).toBe( true );
expect( isNumber( result.seo[ "" ].score ) ).toBe( true );
expect( isObject( result.seo.a ) ).toBe( true );
expect( isArray( result.seo.a.results ) ).toBe( true );
expect( isNumber( result.seo.a.score ) ).toBe( true );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "analyzeRelatedKeywords", { paper: paper.serialize(), relatedKeywords } ) );
} );
test( "listens to the contentAnalysisActive configuration", done => {
const paper = testTexts[ 0 ].paper;
const relatedKeywords = { a: { keyword: "content", synonyms: "" } };
worker.analyzeRelatedKeywordsDone = ( id, result ) => {
// Results still get initialized.
expect( result.readability.results.length ).toBe( 0 );
expect( result.readability.score ).toBe( 0 );
done();
};
scope.onmessage( createMessage( "initialize", { contentAnalysisActive: false } ) );
scope.onmessage( createMessage( "analyzeRelatedKeywords", { paper: paper.serialize(), relatedKeywords } ) );
} );
test( "listens to the keywordAnalysisActive configuration", done => {
const paper = testTexts[ 0 ].paper;
const relatedKeywords = { a: { keyword: "content", synonyms: "" } };
worker.analyzeRelatedKeywordsDone = ( id, result ) => {
// Results still get initialized.
expect( result.seo[ "" ].results.length ).toBe( 0 );
expect( result.seo[ "" ].score ).toBe( 0 );
done();
};
scope.onmessage( createMessage( "initialize", { keywordAnalysisActive: false } ) );
scope.onmessage( createMessage( "analyzeRelatedKeywords", { paper: paper.serialize(), relatedKeywords } ) );
} );
test( "analyze related keywords done calls send", () => {
worker.send = jest.fn();
worker.analyzeRelatedKeywordsDone( 0, { result: true } );
expect( worker.send ).toHaveBeenCalledTimes( 1 );
expect( worker.send ).toHaveBeenCalledWith( "analyzeRelatedKeywords:done", 0, { result: true } );
} );
test( "analyzeRelatedKeywords:failed is called if analyzeRelatedKeywords:done received a result with an error", done => {
const paper = new Paper( "This is the content.", {} );
const relatedKeywords = { a: { keyword: "content", synonyms: "" } };
scope.onmessage( createMessage( "initialize", { logLevel: "trace" } ) );
// Mock the first function call in analyze to throw an error.
worker.shouldReadabilityUpdate = () => {
throw new Error( "Simulated error!" );
};
/*
* Check whether send - which is called from analyzeRelatedKeywords - gets passed the right
* arguments that we expect if analyzeRelatedKeywords failed.
*/
worker.send = ( type, id, payload ) => {
expect( type ).toBe( "analyzeRelatedKeywords:failed" );
expect( id ).toBe( 0 );
expect( isObject( payload ) ).toBe( true );
expect( payload.error ).toBeDefined();
expect( payload.error ).toBe( "An error occurred while running the related keywords analysis.\n\tError: Simulated error!" );
done();
};
scope.onmessage( createMessage( "analyzeRelatedKeywords", { paper: paper.serialize(), relatedKeywords } ) );
} );
} );
describe( "loadScript", () => {
beforeEach( () => {
scope = createScope();
worker = new AnalysisWebWorker( scope, researcher );
worker.register();
} );
test( "schedules a task", () => {
const payload = { url: "http://example.com" };
worker._scheduler.schedule = jest.fn();
scope.onmessage( createMessage( "loadScript", payload ) );
expect( worker._scheduler.schedule ).toHaveBeenCalledTimes( 1 );
expect( worker._scheduler.schedule ).toHaveBeenCalledWith( {
id: 0,
execute: worker.loadScript,
done: worker.loadScriptDone,
data: payload,
type: "loadScript",
} );
} );
test( "calls loadScript", done => {
const payload = { url: "http://example.com" };
const spy = spyOn( worker, "loadScript" );
worker.loadScriptDone = () => {
expect( spy ).toHaveBeenCalledTimes( 1 );
expect( spy ).toHaveBeenCalledWith( 0, payload );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "loadScript", payload ) );
} );
test( "loads a script", done => {
const payload = { url: "http://example.com" };
worker.loadScriptDone = ( id, result ) => {
expect( scope.importScripts ).toHaveBeenCalledTimes( 1 );
expect( scope.importScripts ).toHaveBeenCalledWith( payload.url );
expect( result.loaded ).toBe( true );
expect( result.url ).toBe( payload.url );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "loadScript", payload ) );
} );
test( "handles undefined with a message", done => {
worker.loadScriptDone = ( id, result ) => {
expect( result.loaded ).toBe( false );
expect( result.message ).toBe( "Load Script was called without an URL." );
expect( result.url ).toBeUndefined();
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "loadScript" ) );
} );
test( "handles importScripts error with the error message", done => {
const payload = { url: "http://example.com" };
scope.importScripts = () => {
throw new Error( "Simulated error!" );
};
worker.loadScriptDone = ( id, result ) => {
expect( result.loaded ).toBe( false );
expect( result.message ).toBe( "Simulated error!" );
expect( result.url ).toBe( payload.url );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "loadScript", payload ) );
} );
test( "load script done calls send on success", () => {
worker.send = jest.fn();
worker.loadScriptDone( 0, { loaded: true } );
expect( worker.send ).toHaveBeenCalledTimes( 1 );
expect( worker.send ).toHaveBeenCalledWith( "loadScript:done", 0, { loaded: true } );
} );
test( "load script done calls send on failure", () => {
worker.send = jest.fn();
worker.loadScriptDone( 0, { loaded: false } );
expect( worker.send ).toHaveBeenCalledTimes( 1 );
expect( worker.send ).toHaveBeenCalledWith( "loadScript:failed", 0, { loaded: false } );
} );
} );
describe( "customMessage", () => {
beforeEach( () => {
scope = createScope();
worker = new AnalysisWebWorker( scope, researcher );
worker.register();
} );
test( "schedules a task", () => {
const name = "test";
const payload = { name, data: { test: true } };
worker._scheduler.schedule = jest.fn();
worker._registeredMessageHandlers[ name ] = ( data ) => data;
scope.onmessage( createMessage( "customMessage", payload ) );
expect( worker._scheduler.schedule ).toHaveBeenCalledTimes( 1 );
expect( worker._scheduler.schedule ).toHaveBeenCalledWith( {
id: 0,
execute: worker.customMessage,
done: worker.customMessageDone,
data: payload,
type: "customMessage",
} );
} );
test( "calls customMessage", done => {
const name = "test";
const payload = { name, data: { test: true } };
const spy = spyOn( worker, "customMessage" );
worker._registeredMessageHandlers[ name ] = ( data ) => data;
worker.customMessageDone = () => {
expect( spy ).toHaveBeenCalledTimes( 1 );
expect( spy ).toHaveBeenCalledWith( 0, payload );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "customMessage", payload ) );
} );
test( "handles no registered message handler", done => {
const name = "test";
const payload = { name, data: { test: true } };
worker.customMessageDone = ( id, result ) => {
expect( id ).toBe( 0 );
expect( isObject( result ) ).toBe( true );
expect( result.error ).toBeDefined();
expect( result.error.message ).toBe( "No message handler registered for messages with name: " + name );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "customMessage", payload ) );
} );
test( "handles message handler error", done => {
const name = "test";
const payload = { name, data: { test: true } };
worker._registeredMessageHandlers[ name ] = () => {
throw new Error( "Simulated error!" );
};
worker.customMessageDone = ( id, result ) => {
expect( id ).toBe( 0 );
expect( isObject( result ) ).toBe( true );
expect( result.error ).toBeDefined();
expect( result.error.message ).toBe( "Simulated error!" );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "customMessage", payload ) );
} );
test( "returns custom data", done => {
const name = "test";
const payload = { name, data: { test: true } };
worker._registeredMessageHandlers[ name ] = ( data ) => {
data.handled = "yes";
return data;
};
worker.customMessageDone = ( id, result ) => {
expect( id ).toBe( 0 );
expect( isObject( result ) ).toBe( true );
expect( result.error ).toBeUndefined();
expect( result.data ).toEqual( {
test: true,
handled: "yes",
} );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "customMessage", payload ) );
} );
test( "custom message done calls send on success", () => {
worker.send = jest.fn();
worker.customMessageDone( 0, { success: true, data: "data" } );
expect( worker.send ).toHaveBeenCalledTimes( 1 );
expect( worker.send ).toHaveBeenCalledWith( "customMessage:done", 0, "data" );
} );
test( "custom message done calls send on failure", () => {
worker.send = jest.fn();
worker.customMessageDone( 0, { success: false, error: "failed" } );
expect( worker.send ).toHaveBeenCalledTimes( 1 );
expect( worker.send ).toHaveBeenCalledWith( "customMessage:failed", "failed" );
} );
} );
describe( "runResearch", () => {
beforeEach( () => {
scope = createScope();
worker = new AnalysisWebWorker( scope, researcher );
worker.register();
} );
test( "schedules a task", () => {
const name = "test";
const payload = { name };
worker._scheduler.schedule = jest.fn();
scope.onmessage( createMessage( "runResearch", payload ) );
expect( worker._scheduler.schedule ).toHaveBeenCalledTimes( 1 );
expect( worker._scheduler.schedule ).toHaveBeenCalledWith( {
id: 0,
execute: worker.runResearch,
done: worker.runResearchDone,
data: payload,
} );
} );
test( "calls runResearch", done => {
const name = "test";
const payload = { name };
const spy = spyOn( worker, "runResearch" );
worker.runResearchDone = () => {
expect( spy ).toHaveBeenCalledTimes( 1 );
expect( spy ).toHaveBeenCalledWith( 0, payload );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "runResearch", payload ) );
} );
test( "handles a non-existing research request", done => {
const name = "test";
const payload = { name };
worker.runResearchDone = ( id, result ) => {
expect( id ).toBe( 0 );
expect( result ).toBe( false );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "runResearch", payload ) );
} );
test( "returns the research result", done => {
const name = "findKeywordInFirstParagraph";
const paper = testTexts[ 0 ].paper;
const payload = { name, paper: paper.serialize() };
worker.runResearchDone = ( id, result ) => {
expect( id ).toBe( 0 );
expect( isObject( result ) ).toBe( true );
expect( result.foundInOneSentence ).toBe( true );
expect( result.foundInParagraph ).toBe( true );
expect( result.keyphraseOrSynonym ).toBe( "keyphrase" );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "runResearch", payload ) );
} );
test( "returns the morphology research result without morphologyData", done => {
const name = "morphology";
const paper = testTexts[ 0 ].paper;
const payload = { name, paper: paper.serialize() };
worker.runResearchDone = ( id, result ) => {
expect( id ).toBe( 0 );
expect( isObject( result ) ).toBe( true );
expect( result.keyphraseForms ).toEqual( [ [ "voice" ], [ "search" ] ] );
expect( result.synonymsForms )
.toEqual( [
[ [ "listening" ], [ "reading" ], [ "search" ] ],
[ [ "voice" ], [ "query" ] ],
[ [ "voice" ], [ "results" ] ],
] );
done();
};
scope.onmessage( createMessage( "initialize" ) );
scope.onmessage( createMessage( "runResearch", payload ) );
} );
test( "returns the morphology research result with morphologyData", done => {
const name = "morphology";
const paper = testTexts[ 0 ].paper;
const payload = { name, paper: paper.serialize() };
worker.runResearchDone = ( id, result ) => {
expect( id ).toBe( 0 );
expect( isObject( result ) ).toBe( true );
expect( result.keyphraseForms ).toEqual( [
[ "voice" ],
[ "search" ],
] );
done();
};
scope.onmessage( createMessage( "initialize", { researchData: { morphology: morphologyData } } ) );
scope.onmessage( createMessage( "runResearch", payload ) );
} );
test( "returns an error on research failed", done => {
const name = "firstParagraph";
const payload = { name };
worker._researcher = {
getResearch: () => {
throw new Error( "Research failed" );
},
};
worker.runResearchDone = ( id, result ) => {
expect( id ).toBe( 0 );
expect( isObject( result ) ).toBe( true );
expect( result ).toHaveProperty( "error" );
done();
};
scope.onmessage( createMessage( "initialize", { logLevel: "DEBUG" } ) );
scope.onmessage( createMessage( "runResearch", payload ) );
} );
test( "returns an error on research failed, with a custom error.", done => {
const name = "firstParagraph";
const payload = { name };
worker._researcher = {
getResearch: () => {
throw { error: "This is a custom error." };
},
};
worker.runResearchDone = ( id, result ) => {
expect( id ).toBe( 0 );
expect( isObject( result ) ).toBe( true );
expect( result ).toHaveProperty( "error" );
done();
};
scope.onmessage( createMessage( "initialize", { logLevel: "DEBUG" } ) );
scope.onmessage( createMessage( "runResearch", payload ) );
} );
test( "run research done calls send", () => {
worker.send = jest.fn();
worker.runResearchDone( 0, { result: true } );
expect( worker.send ).toHaveBeenCalledTimes( 1 );
expect( worker.send ).toHaveBeenCalledWith( "runResearch:done", 0, { result: true } );
} );
test( "run research done calls send with error", () => {
worker.send = jest.fn();
worker.runResearchDone( 0, { error: "This is an error." } );
expect( worker.send ).toHaveBeenCalledTimes( 1 );
expect( worker.send ).toHaveBeenCalledWith( "runResearch:failed", 0, { error: "This is an error." } );
} );
} );
} );
describe( "createContentAssessor", () => {
beforeEach( () => {
scope = createScope();
worker = new AnalysisWebWorker( scope, researcher );
} );
test( "listens to contentAnalysisActive", () => {
worker._configuration.contentAnalysisActive = false;
expect( worker.createContentAssessor() ).toBeNull();
worker._configuration.contentAnalysisActive = true;
expect( worker.createContentAssessor() ).not.toBeNull();
} );
test( "listens to useCornerstone", () => {
worker._configuration.useCornerstone = false;
let assessor = worker.createContentAssessor();
expect( assessor ).not.toBeNull();
expect( assessor.type ).toBe( "contentAssessor" );
worker._configuration.useCornerstone = true;
assessor = worker.createContentAssessor();
expect( assessor ).not.toBeNull();
expect( assessor.type ).toBe( "cornerstoneContentAssessor" );
} );
test( "listens to customAnalysisType and sets the custom content assessor if available", () => {
worker._configuration.customAnalysisType = "type1";
// Swapping the content assessor for the SEO assessor.
worker._CustomContentAssessorClasses.type1 = SEOAssessor;
const assessor = worker.createContentAssessor();
// Custom assessor used.
expect( assessor.type ).toBe( "SEOAssessor" );
} );
test( "listens to customAnalysisType but returns the default content assessor if no matching custom assessor is available", () => {
worker._configuration.customAnalysisType = "type1";
// Swapping the content assessor for the SEO assessor.
worker._CustomContentAssessorClasses.type2 = SEOAssessor;
const assessor = worker.createContentAssessor();
// Default assessor used.
expect( assessor.type ).toBe( "contentAssessor" );
} );
test( "listens to customAnalysisType but returns the default content assessor if no custom analysis type is set", () => {
worker._configuration.customAnalysisType = "";
// Swapping the content assessor for the SEO assessor.
worker._CustomContentAssessorClasses.type1 = SEOAssessor;
const assessor = worker.createContentAssessor();
// Default assessor used.
expect( assessor.type ).toBe( "contentAssessor" );
} );
test( "listens to customAnalysisType and sets the custom cornerstone content assessor if available", () => {
worker._configuration.useCornerstone = true;
worker._configuration.customAnalysisType = "type1";
// Swapping the cornerstone assessor for the SEO assessor.
worker._CustomCornerstoneContentAssessorClasses.type1 = SEOAssessor;
const assessor = worker.createContentAssessor();
// Custom assessor used.
expect( assessor.type ).toBe( "SEOAssessor" );
} );
test( "listens to customAnalysisType but returns the default cornerstone SEO assessor if no matching custom assessor is available", () => {
worker._configuration.useCornerstone = true;
worker._configuration.customAnalysisType = "type1";
worker._CustomCornerstoneContentAssessorClasses.type2 = SEOAssessor;
const assessor = worker.createContentAssessor();
// Default assessor used.
expect( assessor.type ).toBe( "cornerstoneContentAssessor" );
} );
test( "listens to customAnalysisType but returns the default cornerstone SEO assessor if no custom analysis type is set", () => {
worker._configuration.useCornerstone = true;
worker._configuration.customAnalysisType = "";
worker._CustomCornerstoneContentAssessorClasses.type1 = SEOAssessor;
const assessor = worker.createContentAssessor();
// Default assessor used.
expect( assessor.type ).toBe( "cornerstoneContentAssessor" );
} );
} );
describe( "createSEOAssessor", () => {
beforeEach( () => {
scope = createScope();
worker = new AnalysisWebWorker( scope, researcher );
} );
test( "listens to keywordAnalysisActive", () => {
worker._configuration.keywordAnalysisActive = false;
expect( worker.createSEOAssessor() ).toBeNull();
worker._configuration.keywordAnalysisActive = true;
expect( worker.createSEOAssessor() ).not.toBeNull();
} );
test( "listens to useCornerstone", () => {
worker._configuration.useCornerstone = false;
let assessor = worker.createSEOAssessor();
expect( assessor ).not.toBeNull();
expect( assessor.type ).toBe( "SEOAssessor" );
worker._configuration.useCornerstone = true;
assessor = worker.createSEOAssessor();
expect( assessor ).not.toBeNull();
expect( assessor.type ).toBe( "cornerstoneSEOAssessor" );
} );
test( "listens to useTaxonomy", () => {
worker._configuration.useTaxonomy = false;
let assessor = worker.createSEOAssessor();
expect( assessor ).not.toBeNull();
expect( assessor.type ).toBe( "SEOAssessor" );
worker._configuration.useTaxonomy = true;
assessor = worker.createSEOAssessor();
expect( assessor ).not.toBeNull();
expect( assessor.type ).toBe( "taxonomyAssessor" );
} );
test( "listens to customAnalysisType and sets the custom SEO assessor if available", () => {
worker._configuration.customAnalysisType = "type1";
// Swapping the SEO assessor for the content assessor.
worker._CustomSEOAssessorClasses.type1 = contentAssessor;
const assessor = worker.createSEOAssessor();
// Custom assessor used.
expect( assessor.type ).toBe( "contentAssessor" );
} );
test( "listens to customAnalysisType but returns the default SEO assessor if no matching custom assessor is available", () => {
worker._configuration.customAnalysisType = "type1";
// Swapping the SEO assessor for the content assessor.
worker._CustomSEOAssessorClasses.type2 = contentAssessor;
const assessor = worker.createSEOAssessor();
// Default assessor used.
expect( assessor.type ).toBe( "SEOAssessor" );
} );
test( "listens to customAnalysisType but returns the default SEO assessor if no custom analysis type is set", () => {
worker._configuration.customAnalysisType = "";
// Swapping the SEO assessor for the content assessor.
worker._CustomSEOAssessorClasses.type2 = contentAssessor;
const assessor = worker.createSEOAssessor();
// Default assessor used.
expect( assessor.type ).toBe( "SEOAssessor" );
} );
test( "listens to customAnalysisType and sets the custom cornerstone SEO assessor if available", () => {
worker._configuration.useCornerstone = true;
worker._configuration.customAnalysisType = "type1";
// Swapping the cornerstone SEO assessor for the content assessor.
worker._CustomCornerstoneSEOAssessorClasses.type1 = contentAssessor;
const assessor = worker.createSEOAssessor();
// Custom assessor used.
expect( assessor.type ).toBe( "contentAssessor" );
} );
test( "listens to customAnalysisType but returns the default cornerstone SEO assessor if no matching custom assessor is available", () => {
worker._configuration.useCornerstone = true;
worker._configuration.customAnalysisType = "type1";
// Swapping the cornerstone SEO assessor for the content assessor.
worker._CustomCornerstoneSEOAssessorClasses.type2 = contentAssessor;
const assessor = worker.createSEOAssessor();
// Default assessor used.
expect( assessor.type ).toBe( "cornerstoneSEOAssessor" );
} );
test( "listens to customAnalysisType but returns the default cornerstone SEO assessor if no custom analysis type is set", () => {
worker._configuration.useCornerstone = true;
worker._configuration.customAnalysisType = "";
// Swapping the cornerstone SEO assessor for the content assessor.
worker._CustomCornerstoneSEOAssessorClasses.type1 = contentAssessor;
const assessor = worker.createSEOAssessor();
// Default a