two tiptap2 custom extensions that extend TextStyle - tiptap

In tiptap2, I have two custom extensions that add a class versus a style because I am utilizing tailwindcss, which leverages classes exclusively, not inline styles.
So, the first one adds 'class="text-green-500"' (or whatever) and likewise, 'class="bg-green-500"'. I extend TextStyle in both custom extensions to allow for class versus span. I believe the answer lies in extending textstyle once, but I'm not sure how to go about that and catch both outcomes.
I can't combine the two by having a highlight span and a color span together.
If I take the following:
and then try and say make the "w" a different color, I get:
What I want to achieve is "Howdy" with complete cyan while still able to apply individual font colors within the outer span (or vice-versa).
import { TextStyle } from '#tiptap/extension-text-style';
export const HighlightColorStyle = TextStyle.extend({
parseHTML() {
return [
{
tag: 'span',
getAttrs: (node) => /text|bg-[\w]*-[1-9]00/.test(node.className)
}
];
}
});
export const HighlightColor = Extension.create({
name: 'highlightColor',
addGlobalAttributes() {
return [
{
types: ['textStyle'],
attributes: {
class: {
default: ''
}
}
}
];
},
addCommands() {
return {
setHighlightColor:
(color) =>
({ chain }) => {
console.log('hoadodoadfaf', color);
return chain().setMark('textStyle', { class: color });
},
toggleHighlightColor:
() =>
({ chain }) => {
return chain().toggleMark('textStyle');
},
unsetHighlightColor:
() =>
({ chain }) => {
return chain().setMark('textStyle', { class: null }).removeEmptyTextStyle();
}
};
}
});
and
import { Extension } from '#tiptap/core';
import { TextStyle } from '#tiptap/extension-text-style';
export const TextColorStyle = TextStyle.extend({
parseHTML() {
return [
{
tag: 'span',
getAttrs: node => /text-[\w]*-[1-9]00/.test(node.className)
}
];
}
});
export const TextColor = Extension.create({
name: 'textColor',
addGlobalAttributes() {
return [
{
types: ['textStyle'],
attributes: {
class: {
default: ''
} }
}
];
},
addCommands() {
return {
setTextColor:
color =>
({ chain }) => {
console.log('hoadodoadfaf', color)
return chain().setMark('textStyle', { class: color });
},
toggleTextColor:
() =>
({ chain }) => {
return chain().toggleMark('textStyle');
},
unsetTextColor:
() =>
({ chain }) => {
return chain().setMark('textStyle', { class: null }).removeEmptyTextStyle();
}
};
}
});

You should treat class as object not string, so the multi class can combine, then add a ClassExtension to apply the class to a tag.
The ClassExtension is works like textStyle, except it will apply the class, textStyle dose not apply styles, because styles is an object, applied by individual Extension.
Write a simple classNames extension based on a modified testStyle extension.

Related

Setting up jest mocks - one way works the other doesn't

When setting up jest mocks for a class what does not work for me with an error of "_TextObj.TextObj is not a constructor" is
import { TextObj, } from "#entities/TextObj";
jest.mock('#entities/TextObj', () => {
return jest.fn().mockImplementation((config: TextObjConfig) => {
return { ...
}
});
});
According to https://jestjs.io/docs/es6-class-mocks#calling-jestmock-with-the-module-factory-parameter I had expected the first version to work too - or not?
however what works is
import { TextObj, } from "#entities/TextObj";
jest.mock('#entities/TextObj');
...
beforeAll(() => {
TextObj.mockImplementation((config: TextObjConfig) => {
return {
..
}
});
});
TextObj is a named export and you're trying to mock default export which is why it is throwing the error _TextObj.TextObj is not a constructor.
For mocking named export, you need to do following the changes i.e return an object that contains TestObj property:
import { TextObj, } from "#entities/TextObj";
jest.mock('#entities/TextObj', () => {
TestObj: jest.fn().mockImplementation((config: TextObjConfig) => {
return { ...
}
});
});

next-i18next Jest Testing with useTranslation

Testing libs...always fun. I am using next-i18next within my NextJS project. We are using the useTranslation hook with namespaces.
When I run my test there is a warning:
console.warn
react-i18next:: You will need to pass in an i18next instance by using initReactI18next
> 33 | const { t } = useTranslation(['common', 'account']);
| ^
I have tried the setup from the react-i18next test examples without success. I have tried this suggestion too.
as well as just trying to mock useTranslation without success.
Is there a more straightforward solution to avoid this warning? The test passes FWIW...
test('feature displays error', async () => {
const { findByTestId, findByRole } = render(
<I18nextProvider i18n={i18n}>
<InviteCollectEmails onSubmit={jest.fn()} />
</I18nextProvider>,
{
query: {
orgId: 666,
},
}
);
const submitBtn = await findByRole('button', {
name: 'account:organization.invite.copyLink',
});
fireEvent.click(submitBtn);
await findByTestId('loader');
const alert = await findByRole('alert');
within(alert).getByText('failed attempt');
});
Last, is there a way to have the translated plain text be the outcome, instead of the namespaced: account:account:organization.invite.copyLink?
Use the following snippet before the describe block OR in beforeEach() to mock the needful.
jest.mock("react-i18next", () => ({
useTranslation: () => ({ t: key => key }),
}));
Hope this helps. Peace.
use this for replace render function.
import { render, screen } from '#testing-library/react'
import DarkModeToggleBtn from '../../components/layout/DarkModeToggleBtn'
import { appWithTranslation } from 'next-i18next'
import { NextRouter } from 'next/router'
jest.mock('react-i18next', () => ({
I18nextProvider: jest.fn(),
__esmodule: true,
}))
const createProps = (locale = 'en', router: Partial<NextRouter> = {}) => ({
pageProps: {
_nextI18Next: {
initialLocale: locale,
userConfig: {
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
},
},
},
} as any,
router: {
locale: locale,
route: '/',
...router,
},
} as any)
const Component = appWithTranslation(() => <DarkModeToggleBtn />)
const defaultRenderProps = createProps()
const renderComponent = (props = defaultRenderProps) => render(
<Component {...props} />
)
describe('', () => {
it('', () => {
renderComponent()
expect(screen.getByRole("button")).toHaveTextContent("")
})
})
I used a little bit more sophisticated approach than mocking to ensure all the functions work the same both in testing and production environment.
First, I create a testing environment:
// testing/env.ts
import i18next, { i18n } from "i18next";
import JSDomEnvironment from "jest-environment-jsdom";
import { initReactI18next } from "react-i18next";
declare global {
var i18nInstance: i18n;
}
export default class extends JSDomEnvironment {
async setup() {
await super.setup();
/* The important part start */
const i18nInstance = i18next.createInstance();
await i18nInstance.use(initReactI18next).init({
lng: "cimode",
resources: {},
});
this.global.i18nInstance = i18nInstance;
/* The important part end */
}
}
I add this environment in jest.config.ts:
// jest.config.ts
export default {
// ...
testEnvironment: "testing/env.ts",
};
Sample component:
// component.tsx
import { useTranslation } from "next-i18next";
export const Component = () => {
const { t } = useTranslation();
return <div>{t('foo')}</div>
}
And later on I use it in tests:
// component.test.tsx
import { setI18n } from "react-i18next";
import { create, act, ReactTestRenderer } from "react-test-renderer";
import { Component } from "./component";
it("renders Component", () => {
/* The important part start */
setI18n(global.i18nInstance);
/* The important part end */
let root: ReactTestRenderer;
act(() => {
root = create(<Component />);
});
expect(root.toJSON()).toMatchSnapshot();
});
I figured out how to make the tests work with an instance of i18next using the renderHook function and the useTranslation hook from react-i18next based on the previous answers and some research.
This is the Home component I wanted to test:
import { useTranslation } from 'next-i18next';
const Home = () => {
const { t } = useTranslation("");
return (
<main>
<div>
<h1> {t("welcome", {ns: 'home'})}</h1>
</div>
</main>
)
};
export default Home;
First, we need to create a setup file for jest so we can start an i18n instance and import the translations to the configuration. test/setup.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import homeES from '#/public/locales/es/home.json';
import homeEN from '#/public/locales/en/home.json';
i18n.use(initReactI18next).init({
lng: "es",
resources: {
en: {
home: homeEN,
},
es: {
home: homeES,
}
},
fallbackLng: "es",
debug: false,
});
export default i18n;
Then we add the setup file to our jest.config.js:
setupFilesAfterEnv: ["<rootDir>/test/setup.ts"]
Now we can try our tests using the I18nextProvider and the useTranslation hook:
import '#testing-library/jest-dom/extend-expect';
import { cleanup, render, renderHook } from '#testing-library/react';
import { act } from 'react-dom/test-utils';
import { I18nextProvider, useTranslation } from 'react-i18next';
import Home from '.';
describe("Index page", (): void => {
afterEach(cleanup);
it("should render properly in Spanish", (): void => {
const t = renderHook(() => useTranslation());
const component = render(
<I18nextProvider i18n={t.result.current.i18n}>
<Home / >
</I18nextProvider>
);
expect(component.getByText("Bienvenido a Pocky")).toBeInTheDocument();
});
it("should render properly in English", (): void => {
const t = renderHook(() => useTranslation());
act(() => {
t.result.current.i18n.changeLanguage("en");
});
const component = render(
<I18nextProvider i18n={t.result.current.i18n}>
<Home/>
</I18nextProvider>
);
expect(component.getByText("Welcome to Pocky")).toBeInTheDocument();
});
});
Here we used the I18nextProvider and send the i18n instance using the useTranslation hook. after that the translations were loaded without problems in the Home component.
We can also change the selected language running the changeLanguage() function and test the other translations.

Accessing context from this

I have a middleware that exports context.isMobile. I can access it from layout like this:
layout (ctx) {
if(ctx.isMobile) {
return 'mobile'
} else if (ctx.isDesktop) {
return 'default'
}
},
...but I can't access the context from data or computed. How do I get the context there?
You can access the context via this.$nuxt.context like this:
export default {
data() {
console.log(this.$nuxt.context)
return { /*...*/ }
},
computed: {
myProp() {
console.log(this.$nuxt.context)
return 'foo'
}
}
}

How do I invoke inquirer.js menu in a loop using Promises?

I wrote a simple Node.js program with a nice menu system facilitated by inquirer.js. However, after selecting an option in the menu and completing some action, the program exits. I need the menu to show again, until I select the Exit [last] option in the menu. I would like to do this using Promise, instead of async/await.
I tried using a function to show the menu and called that function within a forever loop (E.g. while (true) { ... }), but that made the program unusable. I changed that to a for-loop just to observe the problem. Below is the simple program and the resulting output.
PROGRAM
"use strict";
const inquirer = require('inquirer');
const util = require('util')
// Clear the screen
process.stdout.write("\u001b[2J\u001b[0;0H");
const showMenu = () => {
const questions = [
{
type: "list",
name: "action",
message: "What do you want to do?",
choices: [
{ name: "action 1", value: "Action1" },
{ name: "action 2", value: "Action2" },
{ name: "Exit program", value: "quit"}
]
}
];
return inquirer.prompt(questions);
};
const main = () => {
for (let count = 0; count < 3; count++) {
showMenu()
.then(answers => {
if (answers.action === 'Action1') {
return Promise.resolve('hello world');
}
else if (answers.action === 'Action2') {
return new Promise((resolve, reject) => {
inquirer
.prompt([
{
type: 'input',
name: 'secretCode',
message: "Enter a secret code:"
}
])
.then(answers => {
resolve(answers);
})
});
}
else {
console.log('Exiting program.')
process.exit(0);
}
})
.then((data) => { console.log(util.inspect(data, { showHidden: false, depth: null })); })
.catch((error, response) => {
console.error('Error:', error);
});
}
}
main()
OUTPUT
? What do you want to do? (Use arrow keys)
❯ action 1
action 2
Exit program ? What do you want to do? (Use arrow keys)
❯ action 1
action 2
Exit program ? What do you want to do? (Use arrow keys)
❯ action 1
action 2
Exit program (node:983) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 keypress listeners added to [ReadStream]. Use emitter.setMaxListeners() to increase limit
How can I block after the first call to generate the menu, wait for an option to be selected and the corresponding action to complete, and then cycle back to the next iteration of showing the menu?
You can use async/await syntax:
Declare your main function async, and await the returned Promise from inquirer:
const main = async () => {
for (let count = 0; count < 3; count++) {
await showMenu()
.then(answers => {
[...]
}
};
Your code doesn't work as you expect because, in short, the interpreter executes synchronous code before running any callbacks (from promises). As a consequence your synchronous for loop executes before any I/O callbacks are resolved. All calls to showMenu() returns promises which are resolved asynchronously, meaning nothing will be printed, and no inputs will be interpreted until after looping.
Writing await blocks succeeding synchronous code inside an async function, which is what it seems you're trying to do.
Using your code as a starting point, I hacked together my own library for displaying cli menus. It strips away a lot of Inquirer's boilerplate, letting you declare a menu graph/tree concisely.
The main.ts file shows how you use it. You declare a dictionary of MenuPrompts, which you add Menus, Actions and LoopActions to. Each prompt has a key, which other prompts can route to.
// main.ts
import { Menu, Action, MenuPrompt, openMenuPrompt, LoopAction } from "./menus";
// Set of prompts
let prompts = {
menu_1: new MenuPrompt("Menu 1 - This list is ordinal - What would like to do?", 20, true, [
new Menu("Menu 2", "menu_2"),
new LoopAction("Action", () => console.log("Menu 1 action executed")),
new Action("Back", context => context.last),
new Action("Exit", () => process.exit(0)),
]),
menu_2: new MenuPrompt("Menu 2 - This list is NOT ordinal - What would like to do?", 20, false, [
new Menu("Menu 1", "menu_1"),
new LoopAction("Action", () => console.log("Menu 2 action executed")),
new Action("Back", context => context.last),
new Action("Exit", () => process.exit(0)),
]),
};
// Open the "menu_1" prompt
openMenuPrompt("menu_1", prompts);
This is the lib file, which contains types & the function for opening the initial prompt.
// menus.ts
import * as inquirer from "inquirer";
// MAIN FUNCTION
export let openMenuPrompt = async (current: string, prompts: Dict<MenuPrompt>, last?: string): Promise<any> => {
let answer: Answer = (await inquirer.prompt([prompts[current]])).value;
let next = answer.execute({current, last});
if (!next) return;
return await openMenuPrompt(next, prompts, current == next? last : current );
};
// PUBLIC TYPES
export class MenuPrompt {
type = "list";
name = "value";
message: string;
pageSize: number;
choices: Choice[];
constructor(message: string, pageSize: number, isOrdinalList: boolean, choices: Choice[]) {
this.message = message;
this.pageSize = pageSize;
this.choices = choices;
if (isOrdinalList) {
this.choices.forEach((choice, i) => choice.name = `${i + 1}: ${choice.name}`)
}
}
}
export interface Choice {
name: string;
value: Answer;
}
export class Action implements Choice {
name: string;
value: Answer;
constructor(name: string, execute: (context?: MenuContext) => any) {
this.name = name;
this.value = {execute};
}
}
export class LoopAction implements Choice {
name: string;
value: Answer;
constructor(name: string, execute: (context?: MenuContext) => any) {
this.name = name;
this.value = {execute: context => execute(context) ?? context.current};
}
}
export class Menu implements Choice {
name: string;
value: Answer;
constructor(name: string, menuKey: string) {
this.name = name;
this.value = {execute: () => menuKey};
}
}
// INTERNAL TYPES
type Dict<T = any> = {[key: string]: T};
interface Answer {
execute: (context: MenuContext) => any;
}
interface MenuContext {
current: string;
last: string;
}

angular 2 testing kendo-autocomplete

I'm trying to test a component that has kendo-autocomplete control. When the test is ruining the popup with the result doesn't show at all.
What do I need to do?
Below you have the code if you need any other information please let me know.
Component
import { Component, OnInit, Input, Output, Inject } from '#angular/core';
import { IFieldLookUpService } from 'app/services/ifield-look-up.service';
import { FieldLookUpValueResults } from 'app/models/field-look-up-result';
#Component({
selector: 'field-lookup',
templateUrl: './field-lookup.component.html',
styleUrls: ['./field-lookup.component.css']
})
export class FieldLookupComponent implements OnInit {
#Input() fieldId: number;
#Input() fieldName: string;
#Output() selectedValue: string;
private source: FieldLookUpValueResults;
public fieldLookUpValues: FieldLookUpValueResults;
constructor(#Inject('IFieldLookUpService') private fieldLookUpService: IFieldLookUpService) { }
ngOnInit() {
this.loadData();
}
handleFilter(value) {
this.fieldLookUpValues.results = this.source.results.filter((s) => s.text.toLowerCase().indexOf(value.toLowerCase()) !== -1);
}
private loadData() {
this.fieldLookUpService.getLookUpValues(this.fieldId, this.fieldName)
.subscribe(data => { this.source = data;
this.fieldLookUpValues = new FieldLookUpValueResults(this.source.header, null);
})
}
}
Component.html
<div *ngIf="fieldLookUpValues">
<kendo-autocomplete [data]="fieldLookUpValues.results" [valueField]="'text'" [suggest]="true" [value]="selectedValue" [filterable]="true" (filterChange)="handleFilter($event)">
<ng-template kendoAutoCompleteHeaderTemplate>
<strong>{{fieldLookUpValues.header}}</strong>
</ng-template>
</kendo-autocomplete>
</div>
spec
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { DebugElement } from '#angular/core';
import { By } from '#angular/platform-browser';
import { FieldLookupComponent } from './field-lookup.component';
import { FieldLookUpValueResults, FieldLookUpValue } from 'app/models/field-look-up-result';
import { IFieldLookUpService } from 'app/services/ifield-look-up.service';
import { Observable } from 'rxjs/Observable';
import { DropDownsModule } from '#progress/kendo-angular-dropdowns';
fdescribe('FieldLookupComponent', () => {
let component: FieldLookupComponent;
let fixture: ComponentFixture<FieldLookupComponent>;
let debugEl: DebugElement;
let mockFieldLookUpService;
let inputElement;
beforeEach(async(() => {
mockFieldLookUpService = jasmine.createSpyObj('mockFieldLookUpService', ['getLookUpValues']);
let mockData = new FieldLookUpValueResults('LookUp Values Result Header',
[
new FieldLookUpValue('LookUp Value 1', '1'),
new FieldLookUpValue('LookUp Value 2', '2'),
]);
mockFieldLookUpService.getLookUpValues.and.returnValue(Observable.of(mockData));
TestBed.configureTestingModule({
declarations: [ FieldLookupComponent ],
imports: [
DropDownsModule
],
providers: [
{ provide: 'IFieldLookUpService', useFactory: () => mockFieldLookUpService },
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FieldLookupComponent);
component = fixture.componentInstance;
debugEl = fixture.debugElement;
fixture.detectChanges();
inputElement = debugEl.query(By.css('input')).nativeElement;
console.log(component);
});
fit('should be created', () => {
expect(component).toBeTruthy();
});
fit('should have the autocomplete input', () => {
expect(inputElement).toBeTruthy();
});
fdescribe('when character L is set in autocompelte box', () => {
let list: DebugElement;
let listItems: DebugElement[];
beforeEach(() => {
inputElement.value = 'L';
fixture.detectChanges();
list = debugEl.query(By.css('ul')).nativeElement;
listItems = list.queryAll(By.css('li'));
})
fit('should have the kend pop-up shown', () => {
expect(list).toBeTruthy();
});
});
});
I set the value 'L' to the autocomplete input and then I should see the popup but they are null (the list and ListItems)
inputElement.value = 'L';
fixture.detectChanges();
list = debugEl.query(By.css('ul')).nativeElement;
listItems = list.queryAll(By.css('li'));
The Popup component used in the AutoComplete (applicable to other Kendo components with Popup) is appended at the root component by default. In other words, the Popup is not part of the component tree.
For those interested in why this is so, read this Github issue
With those details in mind, you will need to use the AutoComplete instance and retrieve the Popup element from its popupRef property.
{{ autocomplete?.popupRef?.popupElement.nodeName }}
Here is a plunker that demonstrates this approach:
http://plnkr.co/edit/bQTmfBUT7r5z6wjt5MtL?p=preview
Please note that you will need to wait a tick in the tests in order to get the popupRef correctly.
P.S. IMHO, testing the rendered UL list is unneeded. The vendor providing the AutoComplete component has already tested the output items based on the passed [data] value. Considering this fact, I would just test the autocomplete.data property, which should be sufficient.
You can always add functional tests on top of that to ensure that the application you are building is working as a whole.

Resources