diff --git a/AGENTS.md b/AGENTS.md index a86215232..653a4e506 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,3 +10,4 @@ Notes: - Prefer `describe` over `RSpec.describe` in specs. - Do not add `# frozen_string_literal: true` to files. - Prefer multiple small, individually working commits when possible. +- Always run Rubocop on changed Ruby files. diff --git a/npm-api-maker/__tests__/base-model.test.js b/npm-api-maker/__tests__/base-model.test.js index 86d1f44eb..1bc6a814d 100644 --- a/npm-api-maker/__tests__/base-model.test.js +++ b/npm-api-maker/__tests__/base-model.test.js @@ -1,5 +1,4 @@ import BaseModel from "../build/base-model.js" -import CustomError from "../build/custom-error" import {jest} from "@jest/globals" import {JSDOM} from "jsdom" import ValidationError from "../build/validation-error.js" @@ -42,9 +41,19 @@ describe("BaseModel", () => { }) describe("parseValidationErrors", () => { - const error = new CustomError("Some validation error", { + const error = new ValidationError({ + getUnhandledErrorMessage: () => "Some validation error", + getErrorMessage: () => "Some validation error" + }, { response: { - validation_errors: [] + validation_errors: [{ + attribute_name: "name", + attribute_type: "string", + error_messages: ["can't be blank"], + error_types: ["blank"], + input_name: "user[name]", + model_name: "user" + }] } }) const form = document.createElement("form") @@ -52,6 +61,11 @@ describe("BaseModel", () => { const dispatchEventSpy = jest.spyOn(form, "dispatchEvent").mockImplementation(() => "asd") const newCustomEventSpy = jest.spyOn(BaseModel, "newCustomEvent").mockImplementation(() => "asd") + beforeEach(() => { + dispatchEventSpy.mockClear() + newCustomEventSpy.mockClear() + }) + it("throws the validation errors if no options are given", () => { expect(() => BaseModel.parseValidationErrors({error, model})).toThrow(ValidationError) }) @@ -64,8 +78,8 @@ describe("BaseModel", () => { it("doesnt throw validation errors if disabled", () => { BaseModel.parseValidationErrors({error, model, options: {throwValidationError: false}}) - expect(dispatchEventSpy).toHaveBeenCalled() - expect(newCustomEventSpy).toHaveBeenCalled() + expect(dispatchEventSpy).not.toHaveBeenCalled() + expect(newCustomEventSpy).not.toHaveBeenCalled() }) }) }) diff --git a/npm-api-maker/__tests__/cable-connection-pool.test.js b/npm-api-maker/__tests__/cable-connection-pool.test.js index 19c7c1cde..49542741d 100644 --- a/npm-api-maker/__tests__/cable-connection-pool.test.js +++ b/npm-api-maker/__tests__/cable-connection-pool.test.js @@ -1,4 +1,4 @@ -import CableConnectionPool from "../buikd/cable-connection-pool.js" +import CableConnectionPool from "../build/cable-connection-pool.js" import CableSubscriptionPool from "../build/cable-subscription-pool" import {digg} from "diggerize" import {jest} from "@jest/globals" @@ -12,16 +12,16 @@ describe("CableConnectionPool", () => { it("creates a new create event and connects", () => { const cableConnectionPool = new CableConnectionPool() - cableConnectionPool.scheduleConnectUpcoming = function() { - const subscriptionData = this.upcomingSubscriptionData - const subscriptions = this.upcomingSubscriptions + cableConnectionPool.scheduleConnectUpcomingRunLast.queue = () => { + const subscriptionData = cableConnectionPool.upcomingSubscriptionData + const subscriptions = cableConnectionPool.upcomingSubscriptions - this.upcomingSubscriptionData = {} - this.upcomingSubscriptions = {} + cableConnectionPool.upcomingSubscriptionData = {} + cableConnectionPool.upcomingSubscriptions = {} const cableSubscriptionPool = {subscriptionData, subscriptions} - this.cableSubscriptionPools.push(cableSubscriptionPool) + cableConnectionPool.cableSubscriptionPools.push(cableSubscriptionPool) } cableConnectionPool.cableSubscriptionPools = [] cableConnectionPool.connectCreated("Contact", () => console.log("Callback")) @@ -48,16 +48,16 @@ describe("CableConnectionPool", () => { } } - cableConnectionPool.scheduleConnectUpcoming = function() { - const subscriptionData = this.upcomingSubscriptionData - const subscriptions = this.upcomingSubscriptions + cableConnectionPool.scheduleConnectUpcomingRunLast.queue = () => { + const subscriptionData = cableConnectionPool.upcomingSubscriptionData + const subscriptions = cableConnectionPool.upcomingSubscriptions - this.upcomingSubscriptionData = {} - this.upcomingSubscriptions = {} + cableConnectionPool.upcomingSubscriptionData = {} + cableConnectionPool.upcomingSubscriptions = {} const cableSubscriptionPool = {subscriptionData, subscriptions} - this.cableSubscriptionPools.push(cableSubscriptionPool) + cableConnectionPool.cableSubscriptionPools.push(cableSubscriptionPool) } cableConnectionPool.cableSubscriptionPools = [cableSubscriptionPool] cableConnectionPool.connectDestroyed("Contact", "modelId", () => { }) @@ -123,7 +123,7 @@ describe("CableConnectionPool", () => { this.cableSubscriptionPools.push(cableSubscriptionPool) } cableConnectionPool.cableSubscriptionPools = [cableSubscriptionPool] - cableConnectionPool.scheduleConnectUpcoming = () => cableConnectionPool.connectUpcoming() + cableConnectionPool.scheduleConnectUpcomingRunLast.queue = () => cableConnectionPool.connectUpcoming() cableConnectionPool.connectDestroyed("Contact", "modelId", () => { }) const subscriptions = digg(cableSubscriptionPool, "subscriptions", "Contact", "destroys", "modelId") @@ -146,16 +146,16 @@ describe("CableConnectionPool", () => { } } - cableConnectionPool.scheduleConnectUpcoming = function() { - const subscriptionData = this.upcomingSubscriptionData - const subscriptions = this.upcomingSubscriptions + cableConnectionPool.scheduleConnectUpcomingRunLast.queue = () => { + const subscriptionData = cableConnectionPool.upcomingSubscriptionData + const subscriptions = cableConnectionPool.upcomingSubscriptions - this.upcomingSubscriptionData = {} - this.upcomingSubscriptions = {} + cableConnectionPool.upcomingSubscriptionData = {} + cableConnectionPool.upcomingSubscriptions = {} const cableSubscriptionPool = {subscriptionData, subscriptions} - this.cableSubscriptionPools.push(cableSubscriptionPool) + cableConnectionPool.cableSubscriptionPools.push(cableSubscriptionPool) } cableConnectionPool.cableSubscriptionPools = [cableSubscriptionPool] cableConnectionPool.connectUpdate("Contact", "modelId", () => console.log("Update callback")) diff --git a/npm-api-maker/__tests__/collection.test.js b/npm-api-maker/__tests__/collection.test.js index b386a0ad1..49477bc43 100644 --- a/npm-api-maker/__tests__/collection.test.js +++ b/npm-api-maker/__tests__/collection.test.js @@ -1,9 +1,15 @@ import Collection from "../build/collection.js" +class FakeModel { + static modelClassData() { + return {name: "User"} + } +} + describe("Collection", () => { describe("count", () => { it("is able to clone the collection and merge count into it without manipulating the original given query", () => { - let collection = new Collection({}, {}) + let collection = new Collection({modelClass: FakeModel}, {}) collection.ransack({name_cont: "Kasper"}) @@ -19,7 +25,7 @@ describe("Collection", () => { // This can happen if someone does something like this and users_q isn't set: // query.ransack(params.users_q) - let collection = new Collection({}, {ransack: {id_eq: 5}}) + let collection = new Collection({modelClass: FakeModel}, {ransack: {id_eq: 5}}) collection.ransack(undefined) @@ -27,7 +33,7 @@ describe("Collection", () => { }) it("handles sorts of different types", () => { - let collection = new Collection({}, {}) + let collection = new Collection({modelClass: FakeModel}, {}) collection = collection.ransack({s: "created_at"}) expect(collection.queryArgs.ransack.s).toEqual("created_at") @@ -39,7 +45,7 @@ describe("Collection", () => { describe("selectColumns", () => { it("adds selected columns to the query", () => { - let collection = new Collection({}, {}) + let collection = new Collection({modelClass: FakeModel}, {}) collection = collection.selectColumns({User: ["id"]}) expect(collection.queryArgs.selectColumns).toEqual({user: ["id"]}) diff --git a/npm-api-maker/__tests__/custom-error.test.js b/npm-api-maker/__tests__/custom-error.test.js index 1265bd7fe..db69779a2 100644 --- a/npm-api-maker/__tests__/custom-error.test.js +++ b/npm-api-maker/__tests__/custom-error.test.js @@ -7,7 +7,7 @@ describe("CustomError", () => { const customError = new CustomError(`Request failed with code: ${xhr.status}`, {response, xhr}) expect(customError.message).toEqual("Request failed with code: 401") - expect(customError.errorMessages()).toEqual(undefined) + expect(customError.errorMessages()).toEqual(["No error messages found"]) expect(customError.errorTypes()).toEqual(undefined) }) }) diff --git a/npm-api-maker/__tests__/model-name.test.js b/npm-api-maker/__tests__/model-name.test.js index 4c937fc0d..b39428d7d 100644 --- a/npm-api-maker/__tests__/model-name.test.js +++ b/npm-api-maker/__tests__/model-name.test.js @@ -1,4 +1,5 @@ import I18nOnSteroids from "i18n-on-steroids" +import Config from "../build/config" import ModelName from "../build/model-name" const i18n = new I18nOnSteroids() @@ -22,10 +23,11 @@ const initializeI18n = () => { describe("ModelName", () => { beforeEach(() => { initializeI18n() + Config.setI18n(i18n) }) test("human", () => { - const modelClassData = {i18nKey: "user"} + const modelClassData = {i18nKey: "user", name: "User"} const modelName = new ModelName({i18n, modelClassData}) expect(modelName.human()).toEqual("Bruger") diff --git a/npm-api-maker/__tests__/routes-native.test.js b/npm-api-maker/__tests__/routes-native.test.js index 7333426cc..14f4000ee 100644 --- a/npm-api-maker/__tests__/routes-native.test.js +++ b/npm-api-maker/__tests__/routes-native.test.js @@ -12,8 +12,8 @@ const testRoutes = () => ({ {"name": "drinks", "path": "/drinks", "component": "drinks/index"} ] }) -const testTranslations = () => ({ - locales: { +const testTranslations = () => { + const locales = { da: { routes: { drink: "drink", @@ -27,7 +27,16 @@ const testTranslations = () => ({ } } } -}) + + return { + locales, + t: (key, _unused, {default: defaultValue, locale}) => { + const path = key.split(".").slice(1) + const routeKey = path[path.length - 1] + return locales?.[locale]?.routes?.[routeKey] || defaultValue + } + } +} const routesNative = ({args, currentLocale}) => { const test = new RoutesNative({ @@ -66,14 +75,14 @@ describe("RoutesNative", () => { const test = routesNative({args: {localized: true}, currentLocale: "en"}) const daRoute = test.editDrinkPath(5, {drink: {name: "Pina Colada"}, locale: "da"}) - expect(daRoute).toEqual("/da/drinks/5/rediger?drink%5Bname%5D=Pina%20Colada") + expect(daRoute).toEqual("/da/drinks/5/rediger?drink[name]=Pina+Colada") }) it("translates a route without localization", () => { const test = routesNative({currentLocale: "en"}) const daRoute = test.editDrinkPath(5, {drink: {name: "Pina Colada"}}) - expect(daRoute).toEqual("/drinks/5/edit?drink%5Bname%5D=Pina%20Colada") + expect(daRoute).toEqual("/drinks/5/edit?drink[name]=Pina+Colada") }) it("generates urls", () => { @@ -85,20 +94,20 @@ describe("RoutesNative", () => { const test = routesNative({args: {localized: true}, currentLocale: "en"}) const daRoute = test.editDrinkUrl(5, {drink: {name: "Pina Colada"}, locale: "da"}) - expect(daRoute).toEqual("http://localhost/da/drinks/5/rediger?drink%5Bname%5D=Pina%20Colada") + expect(daRoute).toEqual("http://localhost/da/drinks/5/rediger?drink[name]=Pina+Colada") }) it("generates urls with custom options", () => { const test = routesNative({args: {localized: true}, currentLocale: "en"}) const daRoute = test.editDrinkUrl(5, {drink: {name: "Pina Colada"}, locale: "da", host: "google.com", port: 123, protocol: "https"}) - expect(daRoute).toEqual("https://google.com:123/da/drinks/5/rediger?drink%5Bname%5D=Pina%20Colada") + expect(daRoute).toEqual("https://google.com:123/da/drinks/5/rediger?drink[name]=Pina+Colada") }) it("generates urls without locales", () => { const test = routesNative({currentLocale: "en"}) const daRoute = test.editDrinkUrl(5, {drink: {name: "Pina Colada"}, locale: "da", host: "google.com", port: 123, protocol: "https"}) - expect(daRoute).toEqual("https://google.com:123/drinks/5/edit?drink%5Bname%5D=Pina%20Colada") + expect(daRoute).toEqual("https://google.com:123/drinks/5/edit?drink[name]=Pina+Colada") }) }) diff --git a/npm-api-maker/__tests__/setup.js b/npm-api-maker/__tests__/setup.js new file mode 100644 index 000000000..1793cb152 --- /dev/null +++ b/npm-api-maker/__tests__/setup.js @@ -0,0 +1,9 @@ +import {TextDecoder, TextEncoder} from "util" + +if (!global.TextEncoder) { + global.TextEncoder = TextEncoder +} + +if (!global.TextDecoder) { + global.TextDecoder = TextDecoder +} diff --git a/npm-api-maker/__tests__/support/actioncable.js b/npm-api-maker/__tests__/support/actioncable.js new file mode 100644 index 000000000..b0458e3d9 --- /dev/null +++ b/npm-api-maker/__tests__/support/actioncable.js @@ -0,0 +1 @@ +export const createConsumer = () => ({}) diff --git a/npm-api-maker/__tests__/support/models.js b/npm-api-maker/__tests__/support/models.js new file mode 100644 index 000000000..336ce12bb --- /dev/null +++ b/npm-api-maker/__tests__/support/models.js @@ -0,0 +1 @@ +export {} diff --git a/npm-api-maker/babel.config.js b/npm-api-maker/babel.config.cjs similarity index 56% rename from npm-api-maker/babel.config.js rename to npm-api-maker/babel.config.cjs index e4c5a54c4..a6c6d230b 100644 --- a/npm-api-maker/babel.config.js +++ b/npm-api-maker/babel.config.cjs @@ -1,5 +1,12 @@ module.exports = { + sourceType: "unambiguous", presets: [ + [ + "@babel/preset-env", + { + targets: {node: "current"} + } + ], [ "@babel/preset-react", { diff --git a/npm-api-maker/jest.config.cjs b/npm-api-maker/jest.config.cjs new file mode 100644 index 000000000..b8df520e2 --- /dev/null +++ b/npm-api-maker/jest.config.cjs @@ -0,0 +1,15 @@ +module.exports = { + testEnvironment: "jsdom", + testMatch: ["/__tests__/**/*.test.js"], + setupFilesAfterEnv: ["/__tests__/setup.js"], + transform: { + "^.+\\.(cjs|mjs|[jt]sx?)$": ["babel-jest", {configFile: "./babel.config.cjs"}] + }, + transformIgnorePatterns: ["/node_modules/(?!epic-locks|i18n-on-steroids)/"], + moduleNameMapper: { + "^\\.\\./build/(.*)$": "/src/$1", + "^\\.\\./\\.\\./build/(.*)$": "/src/$1", + "^@rails/actioncable$": "/__tests__/support/actioncable.js", + "^models\\.js$": "/__tests__/support/models.js" + } +} diff --git a/npm-api-maker/src/model-prop-type.js b/npm-api-maker/src/model-prop-type.js index 6db8fe1a1..bd6269c15 100644 --- a/npm-api-maker/src/model-prop-type.js +++ b/npm-api-maker/src/model-prop-type.js @@ -1,5 +1,5 @@ import {digg} from "diggerize" -import Inflection from "inflection" +import * as inflection from "inflection" export default class ApiMakerModelPropType { static ofModel (modelClass) { @@ -52,7 +52,7 @@ export default class ApiMakerModelPropType { if (this._withLoadedAbilities) { for (const abilityName of this._withLoadedAbilities) { - const underscoreAbilityName = Inflection.underscore(abilityName) + const underscoreAbilityName = inflection.underscore(abilityName) if (!(underscoreAbilityName in model.abilities)) return new Error(`The ability ${abilityName} was required to be loaded in ${propName} of the ${model.constructor.name} type but it wasn't`) @@ -62,7 +62,7 @@ export default class ApiMakerModelPropType { if (this._withLoadedAssociations) { for (const associationName in this._withLoadedAssociations) { const associationModelPropType = digg(this._withLoadedAssociations, associationName) - const underscoreAssociationName = Inflection.underscore(associationName) + const underscoreAssociationName = inflection.underscore(associationName) if (!(underscoreAssociationName in model.relationshipsCache)) return new Error(`The association ${associationName} was required to be loaded in ${propName} of the ${model.constructor.name} type but it wasn't`) @@ -92,7 +92,7 @@ export default class ApiMakerModelPropType { if (this._withLoadedAttributes && model.isPersisted()) { for (const attributeName of this._withLoadedAttributes) { - const underscoreAttributeName = Inflection.underscore(attributeName) + const underscoreAttributeName = inflection.underscore(attributeName) if (!(underscoreAttributeName in model.modelData)) { return new Error(`The attribute ${attributeName} was required to be loaded in ${propName} of the ${model.constructor.name} type but it wasn't`) diff --git a/npm-api-maker/src/table/table.jsx b/npm-api-maker/src/table/table.jsx index c84afed70..879973bd3 100644 --- a/npm-api-maker/src/table/table.jsx +++ b/npm-api-maker/src/table/table.jsx @@ -1,4 +1,4 @@ -import {digg, digs} from "diggerize" +import {dig, digg, digs} from "diggerize" import React, {createContext, useContext, useMemo, useRef} from "react" import {Animated, Platform, Pressable, View} from "react-native" import BaseComponent from "../base-component" @@ -268,12 +268,17 @@ export default memo(shapeComponent(class ApiMakerTable extends BaseComponent { async loadCurrentWorkplace() { const Workplace = modelClassRequire("Workplace") const result = await Workplace.current() - const currentWorkplace = digg(result, "current", 0) + const currentWorkplace = dig(result, "current", 0) this.setState({currentWorkplace}) } async loadCurrentWorkplaceCount() { + if (!this.s.currentWorkplace) { + this.setState({currentWorkplaceCount: 0}) + return + } + const WorkplaceLink = modelClassRequire("WorkplaceLink") const currentWorkplaceCount = await WorkplaceLink .ransack({ diff --git a/ruby-gem/Gemfile.lock b/ruby-gem/Gemfile.lock index 228b2477a..80a37cc00 100644 --- a/ruby-gem/Gemfile.lock +++ b/ruby-gem/Gemfile.lock @@ -13,7 +13,7 @@ PATH with_advisory_lock PATH - remote: /home/dev/Development/api_maker/api_maker_table_rubygem + remote: /home/dev/api_maker/api_maker_table_rubygem specs: api_maker_table (0.1.1) acts_as_list diff --git a/ruby-gem/spec/api_helpers/api_maker_table_helpers_spec.rb b/ruby-gem/spec/api_helpers/api_maker_table_helpers_spec.rb index 1c6e1e07b..b9b3a0228 100644 --- a/ruby-gem/spec/api_helpers/api_maker_table_helpers_spec.rb +++ b/ruby-gem/spec/api_helpers/api_maker_table_helpers_spec.rb @@ -1,70 +1,77 @@ require "rails_helper" describe "api_maker_table helpers" do - class FakeWorkplace - attr_reader :name + let(:fake_workplace_class) do + Class.new do + attr_reader :name - def initialize(name) - @name = name + def initialize(name) + @name = name + end end end - class FakeUser - attr_reader :id - attr_accessor :current_workplace - - def initialize(id: 1, current_workplace: nil, lock_results: [false]) - @id = id - @current_workplace = current_workplace - @created = false - @lock_results = lock_results - @lock_calls = 0 - end - - def with_advisory_lock(_key) - result = @lock_results[@lock_calls] - @lock_calls += 1 - result = @lock_results.last if result.nil? - return false unless result - - yield - end - - def reload - self - end - - def create_current_workplace!(name:, user:) - @created = true - self.current_workplace = FakeWorkplace.new(name) - self.current_workplace - end - - def save! - true - end - - def created? - @created + let(:fake_user_class) do + workplace_class = fake_workplace_class + + Class.new do + attr_reader :id + attr_accessor :current_workplace + + define_method(:initialize) do |id: 1, current_workplace: nil, lock_results: [false]| + @id = id + @current_workplace = current_workplace + @created = false + @lock_results = lock_results + @lock_calls = 0 + @workplace_class = workplace_class + end + + def with_advisory_lock(_key) + result = @lock_results[@lock_calls] + @lock_calls += 1 + result = @lock_results.last if result.nil? + return false unless result + + yield + end + + def reload + self + end + + def create_current_workplace!(name:, **_kwargs) + @created = true + self.current_workplace = @workplace_class.new(name) + current_workplace + end + + def save! + true + end + + def created? + @created + end end end - class HelperHost - include ApiHelpers::ApiMakerTableHelpers + let(:helper_host_class) do + Class.new do + include ApiHelpers::ApiMakerTableHelpers - def initialize(user) - @current_user = user - end + attr_reader :current_user - def current_user - @current_user + def initialize(user) + @current_user = user + end end end describe "#current_workplace" do it "returns nil when the lock is never acquired" do - user = FakeUser.new(current_workplace: nil, lock_results: [false, false, false]) - helper = HelperHost.new(user) + user = fake_user_class.new(current_workplace: nil, lock_results: [false, false, false]) + helper = helper_host_class.new(user) workplace = helper.current_workplace @@ -73,9 +80,9 @@ def current_user end it "returns existing when the lock is not acquired" do - existing = FakeWorkplace.new("Existing workplace") - user = FakeUser.new(current_workplace: existing, lock_results: [false]) - helper = HelperHost.new(user) + existing = fake_workplace_class.new("Existing workplace") + user = fake_user_class.new(current_workplace: existing, lock_results: [false]) + helper = helper_host_class.new(user) workplace = helper.current_workplace @@ -85,12 +92,12 @@ def current_user end it "creates after retry when the lock is acquired" do - user = FakeUser.new(current_workplace: nil, lock_results: [false, true]) - helper = HelperHost.new(user) + user = fake_user_class.new(current_workplace: nil, lock_results: [false, true]) + helper = helper_host_class.new(user) workplace = helper.current_workplace - expect(workplace).to be_a(FakeWorkplace) + expect(workplace).to be_a(fake_workplace_class) expect(workplace.name).to eq("Current workplace") expect(user).to be_created end