Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mock intl api #474

Merged
merged 3 commits into from
Aug 7, 2023
Merged

Conversation

BenIsenstein
Copy link
Contributor

Purpose (TL;DR) - Mock the global Intl API

Solve the breaking of timer-dependant methods on the Intl global object by including a mocked version

Background (Problem in detail)

Even after calling install(), the Intl.DateTimeFormat() class will currently use the real time of the system, not the time specified by the fake clock.

Affected methods: DateTimeFormat.format(), DateTimeFormat.formatToParts()

My repo demonstrating the problem, using Jest (which uses sinonjs/fake-timers): https://github.com/BenIsenstein/jest_fix_intl_fake_timers

The fix works by following the patterns used to mock the global Date class and mocking the Intl object. A producer function is written to return an Intl object largely identical to the original, except for the DateTimeFormat() class. DateTimeFormat has several methods that are time-sensitive, and depend on the system time when no argument is passed. These are intercepted and made to default to the clock time.

//eslint-disable-next-line jsdoc/require-jsdoc
function createIntl() {
    const ClockIntl = { ...NativeIntl };

    ClockIntl.DateTimeFormat = function (...args) {
        const realFormatter = new NativeIntl.DateTimeFormat(...args);
        const formatter = {};

        ["formatRange", "formatRangeToParts", "resolvedOptions"].forEach(
            (method) => {
                formatter[method] =
                    realFormatter[method].bind(realFormatter);
            }
        );

        ["format", "formatToParts"].forEach((method) => {
            formatter[method] = function (date) {
                return realFormatter[method](date || ClockIntl.clock.now);
            };
        });

        return formatter;
    };

    ClockIntl.DateTimeFormat.prototype = Object.create(
        NativeIntl.DateTimeFormat.prototype
    );
    ClockIntl.DateTimeFormat.prototype.constructor =
        ClockIntl.DateTimeFormat;
    ClockIntl.DateTimeFormat.supportedLocalesOf =
        NativeIntl.DateTimeFormat.supportedLocalesOf;

    return ClockIntl;
}

This fake Intl object is provided to the Global object when install() is called.

Line 1176, inside createClock()

if (intlPresent) {
    clock.Intl = createIntl();
    clock.Intl.clock = clock;
}

and the original Intl object is restored during uninstall().

Line 979 inside hijackMethod()
An else-if clause added to handle hijacking Intl. Because Intl isn't a straightforward global function, rather a global object used like a module, it can't be mocked using the default else clause at the end of hijackMethod(). It has its own clause to set the mocked Intl object globally.

else if (method === "Intl") {
    target[method] = clock[method];

I'm fairly confident on implementation, however less confident on the tests. All advice is appreciated!

Ben

@codecov
Copy link

codecov bot commented Jul 22, 2023

Codecov Report

Patch coverage: 100.00% and project coverage change: +0.13% 🎉

Comparison is base (4203265) 95.66% compared to head (c666cf4) 95.80%.

❗ Current head c666cf4 differs from pull request most recent head 00770f7. Consider uploading reports for the commit 00770f7 to get more accurate results

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #474      +/-   ##
==========================================
+ Coverage   95.66%   95.80%   +0.13%     
==========================================
  Files           2        2              
  Lines         669      691      +22     
==========================================
+ Hits          640      662      +22     
  Misses         29       29              
Flag Coverage Δ
unit 95.80% <100.00%> (+0.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files Changed Coverage Δ
src/fake-timers-src.js 95.80% <100.00%> (+0.14%) ⬆️

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@fatso83
Copy link
Contributor

fatso83 commented Jul 22, 2023

Love when wanted features come with a PR <3 I am off on holiday with just a small mobile screen unsuitable for reviewing code, but hopefully someone else could have a looksie?

@benjamingr
Copy link
Member

I'm on medical leave I'll try to take a look next week and maybe ask Erick from Node to look

@ErickWendel
Copy link

As I don't have much context I think it LGTM

@BenIsenstein
Copy link
Contributor Author

Thanks for the interest folks! I'll look into these failing tests.

@BenIsenstein
Copy link
Contributor Author

Hi all, it looks like the issue in the test suite is being caused by a difference between the mochify and node runtimes. Intl.DateTimeFormat.resolvedOptions() returns a different value between the two. Any suggestions on how to resolve this?

@fatso83
Copy link
Contributor

fatso83 commented Aug 2, 2023

It's not mochify (which is running Chrome headless) vs Node, but specifically Node 14 breaking. You see the tests passing in Node 16 and 18.

If you look at the engine comparisons page for Intl.DateTimeFormat you can see that fractionalSecondDigits was only added in Node 14.6. Guessing we run a slightly older version.

In any case, we only support Node LTS versions, and Node 14 LTS has reached its EOL, so we can just remove that :)

@fatso83
Copy link
Contributor

fatso83 commented Aug 2, 2023

I have now removed Node 14 from main. Does not block this and you do not need to rebase to verify, but now you know 👍

Do you deem it ready for final review and merge?

@BenIsenstein
Copy link
Contributor Author

BenIsenstein commented Aug 2, 2023

Rebased just to keep things clean 👍 yes, it's ready for final review!

Copy link
Contributor

@fatso83 fatso83 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an excellent PR. Really conforms to everything we want, so thank you very much! I'll merge it once the (tiny) changes have been done.

CHANGELOG.md Outdated Show resolved Hide resolved
src/fake-timers-src.js Outdated Show resolved Hide resolved
src/fake-timers-src.js Outdated Show resolved Hide resolved
src/fake-timers-src.js Show resolved Hide resolved
Update author info
@BenIsenstein
Copy link
Contributor Author

Thanks for the review! Changes:

  • removed changelog entry
  • removed IE tautology
  • removed setting constructor (setting the prototype takes care of that already, good catch)
  • added a test for the supportedLocalesOf method

@fatso83 fatso83 merged commit 871f5c8 into sinonjs:main Aug 7, 2023
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants