Skip to content

Commit

Permalink
polish up type tests, finish moving context "unit" tests over to type…
Browse files Browse the repository at this point in the history
… tests.
  • Loading branch information
Filip Maj committed Sep 18, 2024
1 parent 4dac298 commit 4a7a0b7
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 216 deletions.
15 changes: 8 additions & 7 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -795,18 +795,18 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
}

public options<
Source extends OptionsSource = 'block_suggestion',
Source extends OptionsSource = 'block_suggestion', // TODO: here, similarly to `message()`, the generic is the string `type` of the payload. in others, like `action()`, it's the entire payload. could we make this consistent?
MiddlewareCustomContext extends StringIndexed = StringIndexed,
>(
actionId: string | RegExp,
...listeners: Middleware<SlackOptionsMiddlewareArgs<Source>, AppCustomContext & MiddlewareCustomContext>[]
): void;
// TODO: reflect the type in constraints to Source
// TODO: reflect the type in constraints to Source (this relates to the above TODO, too)
public options<
Source extends OptionsSource = OptionsSource,
MiddlewareCustomContext extends StringIndexed = StringIndexed,
>(
constraints: OptionsConstraints,
constraints: OptionsConstraints, // TODO: to be able to 'link' listener arguments to the constrains, should pass the Source type in as a generic here
...listeners: Middleware<SlackOptionsMiddlewareArgs<Source>, AppCustomContext & MiddlewareCustomContext>[]
): void;
// TODO: reflect the type in constraints to Source
Expand All @@ -822,8 +822,8 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
? { action_id: actionIdOrConstraints }
: actionIdOrConstraints;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const _listeners = listeners as any; // FIXME: workaround for TypeScript 4.7 breaking changes
// biome-ignore lint/suspicious/noExplicitAny: FIXME: workaround for TypeScript 4.7 breaking changes
const _listeners = listeners as any;
this.listeners.push([onlyOptions, matchConstraints(constraints), ..._listeners] as Middleware<AnyMiddlewareArgs>[]);
}

Expand All @@ -836,6 +836,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
): void;
public view<
ViewActionType extends SlackViewAction = SlackViewAction,
// TODO: add a type parameter for view constraints; this way we can constrain the handler view arguments based on the type of the constraint, similar to what action() does
MiddlewareCustomContext extends StringIndexed = StringIndexed,
>(
constraints: ViewConstraints,
Expand Down Expand Up @@ -866,8 +867,8 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
return;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const _listeners = listeners as any; // FIXME: workaround for TypeScript 4.7 breaking changes
// biome-ignore lint/suspicious/noExplicitAny: FIXME: workaround for TypeScript 4.7 breaking changes
const _listeners = listeners as any;
this.listeners.push([
onlyViewActions,
matchConstraints(constraints),
Expand Down
8 changes: 4 additions & 4 deletions src/types/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,10 @@ export interface DialogSuggestion extends StringIndexed {
type OptionsAckFn<Source extends OptionsSource> = Source extends 'block_suggestion'
? AckFn<XOR<BlockOptions, OptionGroups<BlockOptions>>>
: Source extends 'interactive_message'
? AckFn<XOR<MessageOptions, OptionGroups<MessageOptions>>>
: AckFn<XOR<DialogOptions, DialogOptionGroups<DialogOptions>>>;
? AckFn<XOR<MessageOptions, OptionGroups<MessageOptions>>>
: AckFn<XOR<DialogOptions, DialogOptionGroups<DialogOptions>>>;

// TODO: why are the next two interfaces identical?
export interface BlockOptions {
options: Option[];
}
Expand Down Expand Up @@ -213,8 +214,7 @@ export interface OptionsRequest<Source extends OptionsSource = OptionsSource> ex
block_id: Source extends 'block_suggestion' ? string : never;
container: Source extends 'block_suggestion' ? StringIndexed : never;

// this appears in the block_suggestions schema, but we're not sure when its present or what its type would be
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: this appears in the block_suggestions schema, but we're not sure when its present or what its type would be
app_unfurl?: any;

// exists for enterprise installs
Expand Down
45 changes: 25 additions & 20 deletions test/types/action.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expectError, expectType } from 'tsd';
import type { BlockElementAction, DialogSubmitAction, InteractiveAction } from '../../';
import { expectAssignable, expectError, expectType } from 'tsd';
import type { SlackAction, BlockElementAction, DialogSubmitAction, InteractiveAction } from '../../';
import App from '../../src/App';

const app = new App({ token: 'TOKEN', signingSecret: 'Signing Secret' });
Expand All @@ -11,23 +11,28 @@ expectError(
}),
);

expectType<void>(
app.action({ type: 'block_actions' }, async ({ action }) => {
expectType<BlockElementAction>(action);
await Promise.resolve(action);
}),
);
app.action({ type: 'block_actions' }, async ({ action }) => {
expectType<BlockElementAction>(action);
});

expectType<void>(
app.action({ type: 'interactive_message' }, async ({ action }) => {
expectType<InteractiveAction>(action);
await Promise.resolve(action);
}),
);
app.action({ type: 'interactive_message' }, async ({ action }) => {
expectType<InteractiveAction>(action);
});

expectType<void>(
app.action({ type: 'dialog_submission' }, async ({ action }) => {
expectType<DialogSubmitAction>(action);
await Promise.resolve(action);
}),
);
app.action({ type: 'dialog_submission' }, async ({ action }) => {
expectType<DialogSubmitAction>(action);
});

interface MyContext {
doesnt: 'matter';
}
// Ensure custom context assigned to individual middleware is honoured
app.action<SlackAction, MyContext>('action_id', async ({ context }) => {
expectAssignable<MyContext>(context);
});

// Ensure custom context assigned to the entire app is honoured
const typedContextApp = new App<MyContext>();
typedContextApp.action('action_id', async ({ context }) => {
expectAssignable<MyContext>(context);
});
25 changes: 18 additions & 7 deletions test/types/command.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { expectType } from 'tsd';
import { expectAssignable, expectType } from 'tsd';
import type { SlashCommand } from '../..';
import App from '../../src/App';

const app = new App({ token: 'TOKEN', signingSecret: 'Signing Secret' });

expectType<void>(
app.command('/hello', async ({ command }) => {
expectType<SlashCommand>(command);
await Promise.resolve(command);
}),
);
app.command('/hello', async ({ command }) => {
expectType<SlashCommand>(command);
});

interface MyContext {
doesnt: 'matter';
}
// Ensure custom context assigned to individual middleware is honoured
app.command<MyContext>('/action', async ({ context }) => {
expectAssignable<MyContext>(context);
});

// Ensure custom context assigned to the entire app is honoured
const typedContextApp = new App<MyContext>();
typedContextApp.command('/action', async ({ context }) => {
expectAssignable<MyContext>(context);
});
168 changes: 81 additions & 87 deletions test/types/options.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,102 @@
import type { Option } from '@slack/types';
import { expectError, expectType } from 'tsd';
import { expectAssignable, expectType } from 'tsd';
import App from '../../src/App';
import type { BlockSuggestion, DialogSuggestion, InteractiveMessageSuggestion, SlackOptions } from '../..';
import type {
AckFn,
BlockOptions,
BlockSuggestion,
DialogOptions,
DialogOptionGroups,
DialogSuggestion,
InteractiveMessageSuggestion,
MessageOptions,
OptionGroups,
} from '../..';

const app = new App({ token: 'TOKEN', signingSecret: 'Signing Secret' });

const blockSuggestionOptions: Option[] = [
{
text: {
type: 'plain_text',
text: 'foo',
},
value: 'bar',
},
];

// set the default to block_suggestion
expectType<void>(
app.options('action-id-or-callback-id', async ({ options, ack }) => {
expectType<BlockSuggestion>(options);
// biome-ignore lint/suspicious/noExplicitAny: TODO: should the callback ID be any? seems wrong
expectType<any>(options.callback_id);
options.block_id;
options.action_id;
// https://github.com/slackapi/bolt-js/issues/720
await ack({ options: blockSuggestionOptions });
await Promise.resolve(options);
}),
);
app.options('action-id-or-callback-id', async ({ options, ack }) => {
// TODO: should BlockSuggestion belong in types package? if so, assertions on its contents should also move to types package.
// defaults options to block_suggestion
expectType<BlockSuggestion>(options);
// biome-ignore lint/suspicious/noExplicitAny: TODO: should the callback ID be any? seems wrong
expectType<any>(options.callback_id);
options.block_id;
options.action_id;
// ack should allow either BlockOptions or OptionGroups
// https://github.com/slackapi/bolt-js/issues/720
expectAssignable<AckFn<BlockOptions>>(ack);
expectAssignable<AckFn<OptionGroups<BlockOptions>>>(ack);
});

// block_suggestion
expectType<void>(
app.options<'block_suggestion'>({ action_id: 'a' }, async ({ options, ack }) => {
expectType<BlockSuggestion>(options);
// https://github.com/slackapi/bolt-js/issues/720
await ack({ options: blockSuggestionOptions });
await Promise.resolve(options);
}),
);
// FIXME: app.options({ type: 'block_suggestion', action_id: 'a' } does not work
// FIXME: app.options({ type: 'block_suggestion', action_id: 'a' } does not constrain the arguments of the handler down to `block_suggestion`

// interactive_message (attachments)
expectType<void>(
app.options<'interactive_message'>({ callback_id: 'a' }, async ({ options, ack }) => {
expectType<InteractiveMessageSuggestion>(options);
ack({ options: blockSuggestionOptions });
await Promise.resolve(options);
}),
);
app.options<'interactive_message'>({ callback_id: 'a' }, async ({ options, ack }) => {
expectType<InteractiveMessageSuggestion>(options);
// ack should allow either MessageOptions or OptionGroups
// https://github.com/slackapi/bolt-js/issues/720
expectAssignable<AckFn<MessageOptions>>(ack);
expectAssignable<AckFn<OptionGroups<MessageOptions>>>(ack);
});

expectType<void>(
app.options({ type: 'interactive_message', callback_id: 'a' }, async ({ options, ack }) => {
// FIXME: the type should be OptionsRequest<'interactive_message'>
expectType<SlackOptions>(options);
// https://github.com/slackapi/bolt-js/issues/720
expectError(ack({ options: blockSuggestionOptions }));
await Promise.resolve(options);
}),
);
// FIXME: app.options({ type: 'interactive_message', callback_id: 'a' } does not constrain the arguments of the handler down to `interactive_message`

// dialog_suggestion (dialog)
expectType<void>(
app.options<'dialog_suggestion'>({ callback_id: 'a' }, async ({ options, ack }) => {
expectType<DialogSuggestion>(options);
// https://github.com/slackapi/bolt-js/issues/720
expectError(ack({ options: blockSuggestionOptions }));
await Promise.resolve(options);
}),
);
// FIXME: app.options({ type: 'dialog_suggestion', callback_id: 'a' } does not work
app.options<'dialog_suggestion'>({ callback_id: 'a' }, async ({ options, ack }) => {
expectType<DialogSuggestion>(options);
// ack should allow either MessageOptions or OptionGroups
// https://github.com/slackapi/bolt-js/issues/720
expectAssignable<AckFn<DialogOptions>>(ack);
expectAssignable<AckFn<DialogOptionGroups<DialogOptions>>>(ack);
});
// FIXME: app.options({ type: 'dialog_suggestion', callback_id: 'a' } does not constrain the arguments of the handler down to `dialog_sggestion`

const db = {
get: (_teamId: string) => {
return [{ label: 'l', value: 'v' }];
},
};

expectType<void>(
// Taken from https://slack.dev/bolt-js/concepts#options
// Example of responding to an external_select options request
app.options('external_action', async ({ options, ack }) => {
// Get information specific to a team or channel
// TODO: modified to satisfy TS compiler; should team be optional?
const results = options.team != null ? db.get(options.team.id) : [];

if (results) {
// (modified to satisfy TS compiler)
const options: Option[] = [];
// Collect information in options array to send in Slack ack response
for (const result of results) {
options.push({
text: {
type: 'plain_text',
text: result.label,
},
value: result.value,
});
}
// Taken from https://slack.dev/bolt-js/concepts#options
// Example of responding to an external_select options request
app.options('external_action', async ({ options, ack }) => {
// Get information specific to a team or channel
// TODO: modified to satisfy TS compiler; should team be optional?
const results = options.team != null ? db.get(options.team.id) : [];

await ack({
options: options,
if (results) {
// (modified to satisfy TS compiler)
const options: Option[] = [];
// Collect information in options array to send in Slack ack response
for (const result of results) {
options.push({
text: {
type: 'plain_text',
text: result.label,
},
value: result.value,
});
} else {
await ack();
}
}),
);

await ack({
options: options,
});
} else {
await ack();
}
});

interface MyContext {
doesnt: 'matter';
}
// Ensure custom context assigned to individual middleware is honoured
app.options<'block_suggestion', MyContext>('suggest', async ({ context }) => {
expectAssignable<MyContext>(context);
});

// Ensure custom context assigned to the entire app is honoured
const typedContextApp = new App<MyContext>();
typedContextApp.options('suggest', async ({ context }) => {
expectAssignable<MyContext>(context);
});
Loading

0 comments on commit 4a7a0b7

Please sign in to comment.