GitHub

@@ -0,0 +1,185 @@

1+

import { newSpecPage } from '@stencil/core/testing';

2+3+

import type { FrameworkDelegate } from '../../../interface';

4+

import { Tab } from '../tab';

5+6+

const mockDelegate = (attachViewToDom: FrameworkDelegate['attachViewToDom']): FrameworkDelegate => ({

7+

attachViewToDom,

8+

removeViewFromDom: jest.fn().mockResolvedValue(undefined),

9+

});

10+11+

const createTab = async (html = '<ion-tab tab="home" component="ion-content"></ion-tab>') => {

12+

const page = await newSpecPage({

13+

components: [Tab],

14+

html,

15+

});

16+17+

return { page, tabEl: page.body.querySelector<HTMLIonTabElement>('ion-tab')! };

18+

};

19+20+

describe('ion-tab: lazy loading', () => {

21+

it('should attach the component only once across multiple setActive calls', async () => {

22+

const { tabEl } = await createTab();

23+

const attachViewToDom = jest.fn().mockResolvedValue(document.createElement('div'));

24+

tabEl.delegate = mockDelegate(attachViewToDom);

25+26+

await tabEl.setActive();

27+

await tabEl.setActive();

28+29+

expect(attachViewToDom).toHaveBeenCalledTimes(1);

30+

});

31+32+

it('should share one attach attempt across concurrent setActive calls', async () => {

33+

const { tabEl } = await createTab();

34+

const attachViewToDom = jest.fn().mockResolvedValue(document.createElement('div'));

35+

tabEl.delegate = mockDelegate(attachViewToDom);

36+37+

await Promise.all([tabEl.setActive(), tabEl.setActive()]);

38+39+

expect(attachViewToDom).toHaveBeenCalledTimes(1);

40+

});

41+42+

it('should make a later setActive wait for the in-flight attach', async () => {

43+

const { tabEl } = await createTab();

44+

let resolveAttach!: (el: HTMLElement) => void;

45+

const attachViewToDom = jest.fn(() => new Promise<HTMLElement>((resolve) => (resolveAttach = resolve)));

46+

tabEl.delegate = mockDelegate(attachViewToDom);

47+48+

const first = tabEl.setActive();

49+

const second = tabEl.setActive();

50+

let secondSettled = false;

51+

second.then(() => (secondSettled = true));

52+53+

await new Promise((resolve) => setTimeout(resolve, 0));

54+55+

expect(secondSettled).toBe(false);

56+57+

resolveAttach(document.createElement('div'));

58+

await Promise.all([first, second]);

59+60+

expect(secondSettled).toBe(true);

61+

expect(attachViewToDom).toHaveBeenCalledTimes(1);

62+

});

63+64+

it('should retry the attach after a failed first attempt', async () => {

65+

const { tabEl } = await createTab();

66+

const attachViewToDom = jest

67+

.fn()

68+

.mockRejectedValueOnce(new Error('attach failed'))

69+

.mockResolvedValue(document.createElement('div'));

70+

tabEl.delegate = mockDelegate(attachViewToDom);

71+72+

await expect(tabEl.setActive()).rejects.toThrow('attach failed');

73+

await expect(tabEl.setActive()).resolves.toBeUndefined();

74+75+

expect(attachViewToDom).toHaveBeenCalledTimes(2);

76+

});

77+78+

it('should not activate the tab when the attach fails', async () => {

79+

const { tabEl } = await createTab();

80+

tabEl.delegate = mockDelegate(jest.fn().mockRejectedValue(new Error('attach failed')));

81+82+

await expect(tabEl.setActive()).rejects.toThrow('attach failed');

83+84+

expect(tabEl.active).toBe(false);

85+

});

86+87+

it('should activate the tab once a retried attach succeeds', async () => {

88+

const { page, tabEl } = await createTab();

89+

const attachViewToDom = jest

90+

.fn()

91+

.mockRejectedValueOnce(new Error('attach failed'))

92+

.mockResolvedValue(document.createElement('div'));

93+

tabEl.delegate = mockDelegate(attachViewToDom);

94+95+

await expect(tabEl.setActive()).rejects.toThrow('attach failed');

96+

await tabEl.setActive();

97+

await page.waitForChanges();

98+99+

expect(attachViewToDom).toHaveBeenCalledTimes(2);

100+

expect(tabEl.active).toBe(true);

101+

expect(tabEl.classList.contains('tab-hidden')).toBe(false);

102+

});

103+104+

it('should attach the component when the tab starts active', async () => {

105+

const { page, tabEl } = await createTab('<ion-tab tab="home" component="ion-content" active="true"></ion-tab>');

106+107+

await page.waitForChanges();

108+109+

expect(tabEl.querySelector('ion-content.ion-page')).not.toBeNull();

110+

expect(tabEl.classList.contains('tab-hidden')).toBe(false);

111+

});

112+113+

it('should not attach anything when no component is provided', async () => {

114+

const { tabEl } = await createTab('<ion-tab tab="home"></ion-tab>');

115+

const attachViewToDom = jest.fn().mockResolvedValue(document.createElement('div'));

116+

tabEl.delegate = mockDelegate(attachViewToDom);

117+118+

await expect(tabEl.setActive()).resolves.toBeUndefined();

119+120+

expect(attachViewToDom).not.toHaveBeenCalled();

121+

expect(tabEl.active).toBe(true);

122+

});

123+124+

describe('when the attach fails on the active watcher path', () => {

125+

let consoleErrorSpy: jest.SpyInstance;

126+127+

beforeEach(() => {

128+

consoleErrorSpy = jest.spyOn(console, 'error');

129+

// Suppress console.error output from polluting the test output

130+

consoleErrorSpy.mockImplementation(() => {});

131+

});

132+133+

afterEach(() => {

134+

consoleErrorSpy.mockRestore();

135+

});

136+137+

// Jest fails a test on an unhandled rejection, so the watcher swallowing the

138+

// rejection is covered without an explicit unhandledRejection listener.

139+

it('should log the error', async () => {

140+

const { page, tabEl } = await createTab();

141+

tabEl.delegate = mockDelegate(jest.fn().mockRejectedValue(new Error('attach failed')));

142+143+

// ion-tabs activates a tab by setting `active`, it does not await setActive().

144+

tabEl.active = true;

145+

await page.waitForChanges();

146+

// Give the rejected attach a turn to settle so the watcher can log it.

147+

await new Promise((resolve) => setTimeout(resolve, 0));

148+149+

expect(consoleErrorSpy).toHaveBeenCalledWith(

150+

'[Ionic Error]: [ion-tab] - Exception in prepareLazyLoaded:',

151+

expect.any(Error)

152+

);

153+

});

154+155+

it('should retry on the next activation, as ion-tabs reactivates tabs', async () => {

156+

const { page, tabEl } = await createTab();

157+

const attachViewToDom = jest

158+

.fn()

159+

.mockRejectedValueOnce(new Error('attach failed'))

160+

.mockImplementation(() => {

161+

const el = document.createElement('div');

162+

el.classList.add('attached-page');

163+

tabEl.appendChild(el);

164+

return Promise.resolve(el);

165+

});

166+

tabEl.delegate = mockDelegate(attachViewToDom);

167+168+

tabEl.active = true;

169+

await page.waitForChanges();

170+

await new Promise((resolve) => setTimeout(resolve, 0));

171+172+

expect(tabEl.querySelector('.attached-page')).toBeNull();

173+174+

// ion-tabs deactivates the leaving tab, so returning to it is a false -> true change.

175+

tabEl.active = false;

176+

await page.waitForChanges();

177+

tabEl.active = true;

178+

await page.waitForChanges();

179+

await new Promise((resolve) => setTimeout(resolve, 0));

180+181+

expect(attachViewToDom).toHaveBeenCalledTimes(2);

182+

expect(tabEl.querySelector('.attached-page')).not.toBeNull();

183+

});

184+

});

185+

});

Read the original on github.com ↗