-
Notifications
You must be signed in to change notification settings - Fork 114
test: increase code coverage across various modules #1409
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
Merged
Merged
Changes from all commits
Commits
Show all changes
46 commits
Select commit
Hold shift + click to select a range
7de196c
test(SessionManager): add missing tests for core functionality
sherwinski c59cf33
test(Badge): `_updateCount` increments/decrements and clears when <=0
sherwinski 5ebd371
test(Bell): add cases for constructor behavior; state transitions
sherwinski ca58f1f
test(Button): add case for `_onClick` concurrency guard
sherwinski 595a93c
test(Dialog): add `_show`/`_hide` tests
sherwinski 54b79c1
test(Launcher): `_activateIfInactive` activates only once; `_inactiva…
sherwinski 497336b
test(Message): `_display` shows message with timeout then hides; test…
sherwinski a352438
test(LoginManager): `_switchingUsersPromise` sequencing; login with t…
sherwinski 35e993e
test(PromptsManager): `_shouldForceSlidedownOverNative` decisions; ev…
sherwinski c9ab2cc
test(TagManager): `_sendTags` calls `OneSignal.User.addTags` when dif…
sherwinski 7b6eb3e
test(helpers): `timeoutPromise` resolves and times out, returns raced…
sherwinski 907acf8
test(IDManager): _createLocalId uses LOCAL_PREFIX and produces UUID-l…
sherwinski 631d230
test(PermissionManager): add tests for core functionality
sherwinski a2e98bc
test: remove unnecessary vitest global imports
sherwinski e10d88b
test: remove unnecessary ts-expext-error comments
sherwinski 8843f8e
test: remove unnecessary vi global import
sherwinski 99fe01c
test(PermissionManager): improve tests
sherwinski 2c98079
test: remove unnecessary `initialize` parameters and `restoreAllMocks`
sherwinski 7c8f9b6
chore(scripts): add task for fixing lint issues
sherwinski 7683709
test: fix linting issues; remove unnecessary `as any` where possible
sherwinski cfd4c99
test(PermissionManager): opt for spies over mocks
sherwinski f123ecd
test(PromptsManager): opt for spies over mocks
sherwinski cdd9765
test(IDManager): update uuid sample to be more similar to a realist s…
sherwinski e8258a7
test(SessionManager): extract out a single ServiceWorker spy
sherwinski b8a396d
test(helpers): extract out fakeTimers; add timeoutPromise describe block
sherwinski d93fb22
test(LoginManager): opt for spies over mocks
sherwinski 2c5f8fc
test(TagManager): opt for spies over mocks
sherwinski dbb9b01
test: avoid use of globalThis in favor of global
sherwinski a455ebe
test(Button): clean up test env initilization step
sherwinski c652c52
test(Bell): extract out install and update spies
sherwinski e86c9ce
test(Button): refactor to use dot notation over brackets
sherwinski 79f7abe
style: fix lint errors
sherwinski d8e82d6
test(Badge): refactor to use dot notation over brackets
sherwinski 2f67558
test(Bell): use toHaveBeenCalledWith for more detailed assert
sherwinski 700361c
test(Button): initialize test env before other test fixtures
sherwinski 668b765
test: add Animations API polyfill to setupTests
sherwinski 95f047c
test(Dialog): extract out `_isPushNotificationsEnabled` spy
sherwinski b42fb7f
test(Dialog): refactor to use dot notation over brackets
sherwinski f2d4d4a
test(Launcher): initialize test env; use dot notation over brackets
sherwinski 0baed68
test(Message): extract out fake timers; use dot notation over brackets
sherwinski 487ecd5
test(SlidedownManager): add back ts-expect-error comment
sherwinski 1c689d6
test(TagManager): clean up spies and asserts
sherwinski f92c6bf
test(LoginManager): clean up mocks
sherwinski 6398323
test(PermissionManager): remove use of `any`
sherwinski 92e84f9
style: fix linting issues
sherwinski 21cd825
test: clean up unused statements; better organize describes
sherwinski File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; | ||
| import Badge from './Badge'; | ||
|
|
||
| describe('Badge', () => { | ||
| beforeEach(() => { | ||
| TestEnvironment.initialize(); | ||
| document.body.innerHTML = ` | ||
| <div class="onesignal-bell-launcher-badge"></div> | ||
| `; | ||
| }); | ||
|
|
||
| test('_updateCount increments and clamps to empty when <= 0', () => { | ||
| const badge = new Badge(); | ||
| // Start at empty, increment twice | ||
| badge._content = ''; | ||
| badge._increment(); | ||
| expect(badge._content).toBe('1'); | ||
| badge._increment(); | ||
| expect(badge._content).toBe('2'); | ||
|
|
||
| // Decrement twice → goes to 0 and clears to empty string | ||
| badge._decrement(); | ||
| expect(badge._content).toBe('1'); | ||
| badge._decrement(); | ||
| expect(badge._content).toBe(''); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; | ||
| import OneSignalEvent from '../../shared/services/OneSignalEvent'; | ||
| import Bell from './Bell'; | ||
| import { BellEvent, BellState } from './constants'; | ||
|
|
||
| // @ts-expect-error - _installEventHooks is not assignable | ||
| const spyInstall = vi.spyOn(Bell.prototype, '_installEventHooks'); | ||
| const updateStateSpy = vi.spyOn(Bell.prototype, '_updateState'); | ||
| describe('Bell', () => { | ||
| beforeEach(() => { | ||
| // Set up OneSignal globals/context to avoid accidental runtime lookups | ||
| TestEnvironment.initialize(); | ||
| }); | ||
|
|
||
| test('constructor early-returns when enable=false and applies defaults', () => { | ||
| const bell = new Bell({ enable: false }); | ||
| expect(bell._options.size).toBe('medium'); | ||
| expect(bell._options.position).toBe('bottom-right'); | ||
| expect(bell._options.theme).toBe('default'); | ||
| expect(spyInstall).not.toHaveBeenCalled(); | ||
| expect(updateStateSpy).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| test('constructor validates and installs hooks when enable=true', () => { | ||
| // Valid non-defaults to ensure validation path runs | ||
| const bell = new Bell({ | ||
| enable: true, | ||
| size: 'small', | ||
| position: 'bottom-left', | ||
| theme: 'inverse', | ||
| showBadgeAfter: 10, | ||
| showLauncherAfter: 1, | ||
| }); | ||
| expect(bell).toBeTruthy(); | ||
| expect(spyInstall).toHaveBeenCalledTimes(1); | ||
| expect(updateStateSpy).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| test('_setState triggers event when changed', () => { | ||
| const bell = new Bell({ enable: false }); | ||
| const trigger = vi.spyOn(OneSignalEvent, '_trigger'); | ||
| // transition should emit | ||
| bell._setState(BellState._Subscribed); | ||
|
|
||
| expect(trigger).toHaveBeenCalledWith(BellEvent._StateChanged, { | ||
| from: BellState._Uninitialized, | ||
| to: BellState._Subscribed, | ||
| }); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; | ||
| import Bell from './Bell'; | ||
| import Button from './Button'; | ||
| import { MessageType } from './constants'; | ||
|
|
||
| describe('Button', () => { | ||
| beforeEach(() => { | ||
| TestEnvironment.initialize(); | ||
| document.body.innerHTML = ` | ||
| <div class="onesignal-bell-launcher-button"></div> | ||
| <div class="onesignal-bell-launcher-message"></div> | ||
| `; | ||
| }); | ||
|
|
||
| test('_onClick concurrency guard and early-return when message showing', async () => { | ||
| const bell = new Bell({ enable: false }); | ||
| const button = new Button(bell); | ||
|
|
||
| // Simulate message being shown of type Message by adding the show class | ||
| const msgEl = document.querySelector( | ||
| '.onesignal-bell-launcher-message', | ||
| ) as HTMLElement; | ||
| msgEl.classList.add('onesignal-bell-launcher-message-opened'); | ||
| bell._message._contentType = MessageType._Message; | ||
|
|
||
| const toggleSpy = vi.spyOn(button, '_toggleDialog'); | ||
|
|
||
| // Force concurrent scenario: set handling to true then call | ||
| button._isHandlingClick = false; | ||
| await button._onClick(); | ||
| expect(toggleSpy).not.toHaveBeenCalled(); | ||
| expect(button._isHandlingClick).toBe(false); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; | ||
| import type { MockInstance } from 'vitest'; | ||
| import Bell from './Bell'; | ||
| import Dialog from './Dialog'; | ||
| import { BellState } from './constants'; | ||
|
|
||
| describe('Dialog', () => { | ||
| let isPushNotificationsEnabledSpy: MockInstance; | ||
|
|
||
| beforeEach(() => { | ||
| TestEnvironment.initialize(); | ||
| document.body.innerHTML = ` | ||
| <div class="onesignal-bell-launcher-dialog"> | ||
| <div class="onesignal-bell-launcher-dialog-body"></div> | ||
| </div> | ||
| `; | ||
| isPushNotificationsEnabledSpy = vi.spyOn( | ||
| OneSignal._context._subscriptionManager, | ||
| '_isPushNotificationsEnabled', | ||
| ); | ||
| }); | ||
|
|
||
| test('_show populates content and toggles shown flag', async () => { | ||
| const bell = new Bell({ enable: false }); | ||
| // Put bell in unsubscribed state to render subscribe button | ||
| bell._state = BellState._Unsubscribed; | ||
| isPushNotificationsEnabledSpy.mockResolvedValue(false); | ||
|
|
||
| const dialog = new Dialog(bell); | ||
| expect(dialog._shown).toBe(false); | ||
| await dialog._show(); | ||
| expect(dialog._shown).toBe(true); | ||
| // Button should be present for subscribe | ||
| expect(dialog._subscribeButton).not.toBeNull(); | ||
| }); | ||
|
|
||
| test('_hide removes shown class and keeps state consistent', async () => { | ||
| const bell = new Bell({ enable: false }); | ||
| bell._state = BellState._Unsubscribed; | ||
| isPushNotificationsEnabledSpy.mockResolvedValue(false); | ||
|
|
||
| const dialog = new Dialog(bell); | ||
| await dialog._show(); | ||
| expect(dialog._shown).toBe(true); | ||
| await dialog._hide(); | ||
| expect(dialog._shown).toBe(false); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; | ||
| import Bell from './Bell'; | ||
| import Launcher from './Launcher'; | ||
|
|
||
| describe('Launcher', () => { | ||
| beforeEach(() => { | ||
| TestEnvironment.initialize(); | ||
| document.body.innerHTML = ` | ||
| <div class="onesignal-bell-launcher"></div> | ||
| `; | ||
| }); | ||
|
|
||
| test('_activateIfInactive sets wasInactive and activates only when inactive', async () => { | ||
| const bell = new Bell({ enable: false }); // disable side-effects | ||
| const launcher = new Launcher(bell); | ||
|
|
||
| // Mark element as inactive by adding inactive class | ||
| launcher._element?.classList.add('onesignal-bell-launcher-inactive'); | ||
| expect(launcher._active).toBe(false); | ||
| await launcher._activateIfInactive(); | ||
| expect(launcher._wasInactive).toBe(true); | ||
| expect(launcher._active).toBe(true); | ||
|
|
||
| // Calling again should be a no-op (remains active, wasInactive unchanged) | ||
| await launcher._activateIfInactive(); | ||
| expect(launcher._wasInactive).toBe(true); | ||
| expect(launcher._active).toBe(true); | ||
| }); | ||
|
|
||
| test('_inactivateIfWasInactive only fires when previously inactive path set', async () => { | ||
| const bell = new Bell({ enable: false }); | ||
| const launcher = new Launcher(bell); | ||
| // Mark that it was activated from inactive state | ||
| launcher._wasInactive = true; | ||
|
|
||
| await launcher._inactivateIfWasInactive(); | ||
| expect(launcher._wasInactive).toBe(false); | ||
| expect(launcher._active).toBe(false); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; | ||
| import Bell from './Bell'; | ||
| import Message from './Message'; | ||
|
|
||
| vi.useFakeTimers(); | ||
|
|
||
| describe('Message', () => { | ||
| beforeEach(() => { | ||
| TestEnvironment.initialize(); | ||
| document.body.innerHTML = ` | ||
| <div class="onesignal-bell-launcher-message"> | ||
| <div class="onesignal-bell-launcher-message-body"></div> | ||
| </div> | ||
| <div class="onesignal-bell-launcher-badge"></div> | ||
| `; | ||
| }); | ||
|
|
||
| test('_display shows then hides after duration and resets content type', async () => { | ||
| const bell = new Bell({ enable: false }); | ||
| const message = new Message(bell); | ||
| const promise = message._display('message', 'Hello', 1000); | ||
| // advance timers to cover delay | ||
| await vi.runOnlyPendingTimersAsync(); | ||
| await promise; | ||
| // After display finishes, contentType should reset to 'tip' | ||
| expect(message._contentType).toBe('tip'); | ||
| expect(message._shown).toBe(false); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; | ||
| import { updateIdentityModel } from '__test__/support/helpers/setup'; | ||
| import { SubscriptionModel } from 'src/core/models/SubscriptionModel'; | ||
| import { db } from 'src/shared/database/client'; | ||
| import Log from 'src/shared/libraries/Log'; | ||
| import * as userDirector from '../../onesignal/userDirector'; | ||
| import LoginManager from './LoginManager'; | ||
|
|
||
| const createUserOnServerSpy = vi.spyOn(userDirector, 'createUserOnServer'); | ||
|
|
||
| describe('LoginManager', () => { | ||
| beforeEach(() => { | ||
| TestEnvironment.initialize(); | ||
| }); | ||
|
|
||
| test('login: skips when externalId unchanged and logs debug', async () => { | ||
| const debugSpy = vi | ||
| .spyOn(Log, '_debug') | ||
| .mockImplementation(() => undefined); | ||
| await updateIdentityModel('external_id', 'same-id'); | ||
|
|
||
| await LoginManager.login('same-id'); | ||
| expect(debugSpy).toHaveBeenCalledWith( | ||
| 'Login: External ID already set, skipping login', | ||
| ); | ||
| }); | ||
|
|
||
| test('login: stores token when provided and enqueues operations', async () => { | ||
| const dbSpy = vi.spyOn(db, 'put'); | ||
| // mock push subscription exists so transfer op enqueues | ||
| const createPushSub = () => ({ | ||
| id: 'push-sub-id', | ||
| }); | ||
| vi.spyOn( | ||
| OneSignal._coreDirector, | ||
| '_getPushSubscriptionModel', | ||
| ).mockResolvedValue(createPushSub() as SubscriptionModel); | ||
| const enqueueSpy = vi.spyOn( | ||
| OneSignal._coreDirector._operationRepo, | ||
| '_enqueue', | ||
| ); | ||
| const enqueueAndWaitSpy = vi | ||
| .spyOn(OneSignal._coreDirector._operationRepo, '_enqueueAndWait') | ||
| .mockResolvedValue(undefined); | ||
|
|
||
| await LoginManager.login('new-external-id', 'jwt-token-123'); | ||
| expect(dbSpy).toHaveBeenCalledWith('Ids', { | ||
| id: 'jwt-token-123', | ||
| type: 'jwtToken', | ||
| }); | ||
| expect(enqueueSpy).toHaveBeenCalled(); | ||
| expect(enqueueAndWaitSpy).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| test('logout: no external id logs debug and returns', async () => { | ||
| const debugSpy = vi | ||
| .spyOn(Log, '_debug') | ||
| .mockImplementation(() => undefined); | ||
| await updateIdentityModel('external_id', undefined); | ||
| await LoginManager.logout(); | ||
| expect(debugSpy).toHaveBeenCalledWith( | ||
| 'Logout: User is not logged in, skipping logout', | ||
| ); | ||
| }); | ||
|
|
||
| test('logout: with external id resets models and creates anonymous user', async () => { | ||
| await updateIdentityModel('external_id', 'abc'); | ||
| await LoginManager.logout(); | ||
| expect(createUserOnServerSpy).toHaveBeenCalled(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; | ||
| import { setupLoadStylesheet } from '__test__/support/helpers/setup'; | ||
| import { Browser } from 'src/shared/useragent/constants'; | ||
| import * as detect from 'src/shared/useragent/detect'; | ||
| import { PromptsManager } from './PromptsManager'; | ||
|
|
||
| const getBrowserNameSpy = vi.spyOn(detect, 'getBrowserName'); | ||
| const getBrowserVersionSpy = vi.spyOn(detect, 'getBrowserVersion'); | ||
| const isMobileBrowserSpy = vi.spyOn(detect, 'isMobileBrowser'); | ||
| const isTabletBrowserSpy = vi.spyOn(detect, 'isTabletBrowser'); | ||
| const requiresUserInteractionSpy = vi.spyOn(detect, 'requiresUserInteraction'); | ||
|
|
||
| describe('PromptsManager', () => { | ||
| beforeEach(() => { | ||
| TestEnvironment.initialize(); | ||
| }); | ||
|
|
||
| test('_shouldForceSlidedownOverNative returns true on Chrome>=63 mobile/tablet', async () => { | ||
| getBrowserNameSpy.mockReturnValue(Browser._Chrome); | ||
| getBrowserVersionSpy.mockReturnValue(70); | ||
| isMobileBrowserSpy.mockReturnValue(true); | ||
| isTabletBrowserSpy.mockReturnValue(false); | ||
| requiresUserInteractionSpy.mockReturnValue(false); | ||
|
|
||
| const pm = new PromptsManager(OneSignal._context); | ||
| expect(pm['_shouldForceSlidedownOverNative']()).toBe(true); | ||
| }); | ||
|
|
||
| test('_shouldForceSlidedownOverNative returns true when requiresUserInteraction', async () => { | ||
| getBrowserNameSpy.mockReturnValue(Browser._Firefox); | ||
| getBrowserVersionSpy.mockReturnValue(100); | ||
| isMobileBrowserSpy.mockReturnValue(false); | ||
| isTabletBrowserSpy.mockReturnValue(false); | ||
| requiresUserInteractionSpy.mockReturnValue(true); | ||
|
|
||
| const pm = new PromptsManager(OneSignal._context); | ||
| expect(pm['_shouldForceSlidedownOverNative']()).toBe(true); | ||
| }); | ||
|
|
||
| test('event hooks install only once for slidedown path', async () => { | ||
| await setupLoadStylesheet(); | ||
| const pm = new PromptsManager(OneSignal._context); | ||
|
|
||
| // stub _createSlidedown to avoid side effects | ||
| vi.spyOn( | ||
| OneSignal._context._slidedownManager, | ||
| '_createSlidedown', | ||
| ).mockResolvedValue(undefined); | ||
| const installSpy = vi.spyOn(pm, '_installEventHooksForSlidedown'); | ||
|
|
||
| await pm['_internalShowSlidedownPrompt'](); | ||
| await pm['_internalShowSlidedownPrompt'](); | ||
| expect(installSpy).toHaveBeenCalledTimes(1); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -305,16 +305,16 @@ | |
| OneSignal._didLoadITILibrary = true; | ||
|
|
||
| // @ts-expect-error - mock intl-tel-input | ||
| window.intlTelInput = vi.fn().mockImplementation((input) => ({ | ||
| getNumber: () => '+1234567890', // Return formatted number | ||
| isValidNumber: () => true, | ||
| getNumberType: () => 0, | ||
| destroy: () => {}, | ||
| })); | ||
|
|
||
| window.intlTelInputUtils = { | ||
| numberType: { MOBILE: 1 }, | ||
| // @ts-expect-error - mock intl-tel-input | ||
| // @ts-ignore @ts-expect-error - mock intl-tel-input | ||
|
Check failure on line 317 in src/page/managers/slidedownManager/SlidedownManager.test.ts
|
||
| numberFormat: { E164: 0 }, | ||
| }; | ||
| return Promise.resolve(); | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.