diff --git a/client/src/utils/__fixtures/curriculum-helpers-css.tsx b/client/src/utils/__fixtures/curriculum-helpers-css.ts similarity index 100% rename from client/src/utils/__fixtures/curriculum-helpers-css.tsx rename to client/src/utils/__fixtures/curriculum-helpers-css.ts diff --git a/client/src/utils/__fixtures/curriculum-helpers-html.tsx b/client/src/utils/__fixtures/curriculum-helpers-html.ts similarity index 100% rename from client/src/utils/__fixtures/curriculum-helpers-html.tsx rename to client/src/utils/__fixtures/curriculum-helpers-html.ts diff --git a/client/src/utils/__fixtures/curriculum-helpers-javascript.tsx b/client/src/utils/__fixtures/curriculum-helpers-javascript.ts similarity index 67% rename from client/src/utils/__fixtures/curriculum-helpers-javascript.tsx rename to client/src/utils/__fixtures/curriculum-helpers-javascript.ts index 0086ae638e..1d11602a6a 100644 --- a/client/src/utils/__fixtures/curriculum-helpers-javascript.tsx +++ b/client/src/utils/__fixtures/curriculum-helpers-javascript.ts @@ -31,11 +31,40 @@ function nonMutatingPush(original, newItem) { return original.push(newItem); }`; +const jsCodeWithNoCall = `function myFunc() { + return Math.random(); +} +`; + +const jsCodeWithNoArgCall = `function myFunc() { + return Math.random(); +} +myFunc(); +`; + +const jsCodeWithArgCall = `function myFunc() { + return Math.random(); +} +myFunc('this shouldn't be here'); +`; + +const jsCodeWithCommentedCall = `function myFunc() { + return Math.random(); +} +/* +myFunc(); +*/ +`; + const testValues = { jsCodeWithSingleAndMultLineComments, jsCodeWithSingleAndMultLineCommentsRemoved, jsCodeWithUrl, - jsCodeWithUrlUnchanged + jsCodeWithUrlUnchanged, + jsCodeWithNoCall, + jsCodeWithNoArgCall, + jsCodeWithArgCall, + jsCodeWithCommentedCall }; export default testValues; diff --git a/client/src/utils/__fixtures/curriculum-helpers-remove-white-space.tsx b/client/src/utils/__fixtures/curriculum-helpers-remove-white-space.ts similarity index 100% rename from client/src/utils/__fixtures/curriculum-helpers-remove-white-space.tsx rename to client/src/utils/__fixtures/curriculum-helpers-remove-white-space.ts diff --git a/client/src/utils/curriculum-helpers.test.ts b/client/src/utils/curriculum-helpers.test.ts index 38465b6a5c..a61f2c6165 100644 --- a/client/src/utils/curriculum-helpers.test.ts +++ b/client/src/utils/curriculum-helpers.test.ts @@ -15,7 +15,11 @@ const { jsCodeWithSingleAndMultLineComments, jsCodeWithSingleAndMultLineCommentsRemoved, jsCodeWithUrl, - jsCodeWithUrlUnchanged + jsCodeWithUrlUnchanged, + jsCodeWithNoCall, + jsCodeWithNoArgCall, + jsCodeWithArgCall, + jsCodeWithCommentedCall } = jsTestValues; describe('removeWhiteSpace', () => { @@ -83,3 +87,22 @@ describe('removeHtmlComments', () => { ); }); }); + +describe('isCalledWithNoArgs', () => { + const { isCalledWithNoArgs } = __testHelpers; + it('returns a boolean', () => { + expect(typeof isCalledWithNoArgs('foo', 'bar')).toBe('boolean'); + }); + it('returns false when not called', () => { + expect(isCalledWithNoArgs('myFunc', jsCodeWithNoCall)).toBe(false); + }); + it('returns true for a call with no arguments', () => { + expect(isCalledWithNoArgs('myFunc', jsCodeWithNoArgCall)).toBe(true); + }); + it('returns false for a call with arguments', () => { + expect(isCalledWithNoArgs('myFunc', jsCodeWithArgCall)).toBe(false); + }); + it('returns false for a commented out call', () => { + expect(isCalledWithNoArgs('myFunc', jsCodeWithCommentedCall)).toBe(false); + }); +}); diff --git a/client/src/utils/curriculum-helpers.ts b/client/src/utils/curriculum-helpers.ts index 79e7bd1d48..6e0211d0dc 100644 --- a/client/src/utils/curriculum-helpers.ts +++ b/client/src/utils/curriculum-helpers.ts @@ -21,10 +21,37 @@ const removeWhiteSpace = (str = ''): string => { return str.replace(/\s/g, ''); }; +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping +function escapeRegExp(exp: string): string { + return exp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/* +This helper checks if a function/method is called with no arguments. + +Because Safari does not support lookbehinds (as of writing this on +July 14 2021), avoiding false matches on function definitions is done by +checking that only whitespace characters preceed the calling name on the line +it is found on. That makes this helper incompatible with +removeWhiteSpace() above, which removes all whitespace characters. +*/ +function isCalledWithNoArgs( + calledFuncName: string, + callingCode: string +): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const noCommentsCallingCode = strip(callingCode) as string; + const funcExp = `^\\s*?${escapeRegExp(calledFuncName)}\\(\\s*?\\)`; + const matches = new RegExp(funcExp, 'gm').exec(noCommentsCallingCode) ?? []; + + return !!matches.length; +} + const curriculumHelpers = { removeHtmlComments, removeCssComments, removeWhiteSpace, + isCalledWithNoArgs, CSSHelp }; diff --git a/curriculum/challenges/english/03-front-end-libraries/react/create-a-controlled-form.md b/curriculum/challenges/english/03-front-end-libraries/react/create-a-controlled-form.md index efb076c172..890f40c7db 100644 --- a/curriculum/challenges/english/03-front-end-libraries/react/create-a-controlled-form.md +++ b/curriculum/challenges/english/03-front-end-libraries/react/create-a-controlled-form.md @@ -101,19 +101,11 @@ Submitting the form should run `handleSubmit` which should set the `submit` prop `handleSubmit` should call `event.preventDefault` ```js -const handleSubmit = MyForm.prototype.handleSubmit.toString(); -const allMatches = handleSubmit.match(/\bevent\.preventDefault\(\s*?\)/g) ?? []; -const blockCommented = handleSubmit.match( - /\/\*.*?\bevent\.preventDefault\(\s*?\).*?\*\//gs -); -const lineCommented = handleSubmit.match( - /\/\/.*?\bevent\.preventDefault\(\s*?\)/g -); -const commentedMatches = [...(blockCommented ?? []), ...(lineCommented ?? [])]; - assert( - // At least one event.preventDefault() call exists and is not commented out - allMatches.length > commentedMatches.length + __helpers.isCalledWithNoArgs( + 'event.preventDefault', + MyForm.prototype.handleSubmit.toString() + ) ); ```